# ArcQML Technical Manual

> Applicable version: `ArcQML 0.1.0` (experimental Windows preview). ArcQML uses a hybrid noncommercial source-available license, not an OSI-approved open-source license. Public source, the official closed-source Runtime, and third-party components each retain their own licenses.

## Release status and license {#license}

ArcQML uses hybrid licensing. This is a source-available release, not an OSI-approved open-source release:

- Original public source is covered by the [ArcQML Noncommercial Source License](https://github.com/Quill-ArcLight/ArcQML/blob/HEAD/LICENSE). Only noncommercial use is permitted; modifications and integrated works must disclose their complete source under its terms. Commercial use requires a separate license.
- The official closed-source Runtime is covered by the [Noncommercial Binary License](https://github.com/Quill-ArcLight/ArcQML/blob/HEAD/LICENSE-RUNTIME). The public-source license grants an exception for the unmodified official Runtime, so its private implementation need not be disclosed. User integration code must still be disclosed.
- The public framework, closed-source Runtime, and third-party components within a wheel retain their respective licenses. Third-party material retains its original licenses; see [Third-party notices](https://github.com/Quill-ArcLight/ArcQML/blob/HEAD/THIRD_PARTY_NOTICES.md), the versioned license texts, and the SBOM (software bill of materials) included in the package.
- Rights already granted for lawfully obtained MIT / Apache copies are not revoked by this release-policy change.

Maintainer: liuxl. Commercial licensing and license inquiries: [quill@arclightquantum.com](mailto:quill@arclightquantum.com). See [Release status and license in the ArcQML repository](https://github.com/Quill-ArcLight/ArcQML#发行状态与许可证) for complete details.

This is an experimental Windows preview using Rust 1.98.0 and CPython 3.11. Memory-safety issue S01, Linux acceptance, and formatting cleanup are deferred; see the [Release notes](https://github.com/Quill-ArcLight/ArcQML/blob/HEAD/RELEASE_NOTES.md) and [Security limitations](https://github.com/Quill-ArcLight/ArcQML/blob/HEAD/SECURITY.md). Use only the libraries and wheel supplied with this release; do not mix historical artifacts.

## Contents

- [1. Scope and capabilities](#1-scope-and-capabilities)
  - [1.1 Framework overview](#11-framework-overview)
  - [1.2 Supported capabilities](#12-supported-capabilities)
  - [1.3 Current limitations](#13-current-limitations)
- [2. Release package and environment setup](#2-release-package-and-environment-setup)
  - [2.1 Release directory](#21-release-directory)
  - [2.2 Rust environment and Runtime configuration](#22-rust-environment-and-runtime-configuration)
  - [2.3 Python environment and wheel installation](#23-python-environment-and-wheel-installation)
  - [2.4 Data flow through a trainable forward and backward pass](#24-data-flow-through-a-trainable-forward-and-backward-pass)
- [3. Mathematical, indexing, and storage conventions](#3-mathematical-indexing-and-storage-conventions)
  - [3.1 Pure states and normalization](#31-pure-states-and-normalization)
  - [3.2 Qubit indices and binary-string display](#32-qubit-indices-and-binary-string-display)
  - [3.3 Local matrices and multi-qubit gate order](#33-local-matrices-and-multi-qubit-gate-order)
- [4. Tensor, Parameter, and automatic differentiation](#4-tensor-parameter-and-automatic-differentiation)
  - [4.1 Tensor data and metadata](#41-tensor-data-and-metadata)
  - [4.2 Autograd graphs and gradient lifetimes](#42-autograd-graphs-and-gradient-lifetimes)
  - [4.3 Parameter semantics](#43-parameter-semantics)
- [5. Circuit, Operation, and parameter management](#5-circuit-operation-and-parameter-management)
  - [5.1 Circuit structure](#51-circuit-structure)
  - [5.2 Built-in gates](#52-built-in-gates)
  - [5.3 Three ways to add gate parameters](#53-three-ways-to-add-gate-parameters)
  - [5.4 Shared parameters and gradient accumulation](#54-shared-parameters-and-gradient-accumulation)
  - [5.5 Validation, copying, and concatenation](#55-validation-copying-and-concatenation)
  - [5.6 Current limits on parameter expressions](#56-current-limits-on-parameter-expressions)
- [6. Pauli observables and Hamiltonians](#6-pauli-observables-and-hamiltonians)
  - [6.1 PauliString](#61-paulistring)
  - [6.2 SparsePauliOp and compilation caching](#62-sparsepauliop-and-compilation-caching)
  - [6.3 Expectation semantics](#63-expectation-semantics)
- [7. Single and batched state-vector simulation](#7-single-and-batched-state-vector-simulation)
  - [7.1 StateVectorSimulator: stateful and stateless interfaces](#71-statevectorsimulator-stateful-and-stateless-interfaces)
  - [7.2 Importing and validating initial states](#72-importing-and-validating-initial-states)
  - [7.3 BatchStateVectorSimulator](#73-batchstatevectorsimulator)
- [8. Quantum adjoint differentiation](#8-quantum-adjoint-differentiation)
  - [8.1 Forward definition](#81-forward-definition)
  - [8.2 Single-state adjoint backward formula](#82-single-state-adjoint-backward-formula)
  - [8.3 Batch upstream gradients and VJPs](#83-batch-upstream-gradients-and-vjps)
  - [8.4 no_grad and training-graph memory](#84-no_grad-and-training-graph-memory)
- [9. State analysis and terminal sampling](#9-state-analysis-and-terminal-sampling)
  - [9.1 Exact and marginal probabilities](#91-exact-and-marginal-probabilities)
  - [9.2 Pure-state fidelity and Bloch vectors](#92-pure-state-fidelity-and-bloch-vectors)
  - [9.3 Exact behavior of sample_counts](#93-exact-behavior-of-sample_counts)
- [10. Loss functions, optimizers, and training loops](#10-loss-functions-optimizers-and-training-loops)
  - [10.1 Losses: input constraints and forward formulas](#101-losses-input-constraints-and-forward-formulas)
  - [10.2 Optimizers](#102-optimizers)
  - [10.3 Recommended training loop](#103-recommended-training-loop)
  - [10.4 Custom losses](#104-custom-losses)
- [11. Weight checkpoints and full-unitary fitting](#11-weight-checkpoints-and-full-unitary-fitting)
  - [11.1 Weight checkpoint file contract](#111-weight-checkpoint-file-contract)
  - [11.2 DenseUnitary data contract](#112-denseunitary-data-contract)
  - [11.3 Unitary fidelity, loss, and analytical gradients](#113-unitary-fidelity-loss-and-analytical-gradients)
- [12. Visualization, Python bindings, and parallel execution](#12-visualization-python-bindings-and-parallel-execution)
  - [12.1 Text and SVG circuit diagrams](#121-text-and-svg-circuit-diagrams)
  - [12.2 Current Python exports](#122-current-python-exports)
  - [12.3 Rayon thread pool](#123-rayon-thread-pool)

## 1. Scope and capabilities

### 1.1 Framework overview

ArcQML is a quantum machine-learning framework implemented natively in Rust, with Rust and Python interfaces. A unified interface supports fixed and parameterized circuits, state-vector evolution of single or batched quantum states, and observables defined by Pauli operators and their linear combinations. Circuit outputs are differentiable `Tensor` objects: expectation values can be read directly or used in losses, classical tensor operations, and other differentiable computations, joining quantum and classical computation in one automatic differentiation chain.

For parameterized circuits, ArcQML Runtime provides native numerical gate application, gate derivatives, circuit-plan execution, and adjoint backpropagation. The public Rust layer handles circuits and parameters, observable compilation and expectation evaluation, outer batch scheduling, autograd graphs, losses, and optimizers. After `backward`, gradients flow from the classical loss through the graph to circuit parameters, which optimizers such as `Adam` or `SGD` update. These capabilities support variational quantum eigensolvers, quantum neural networks, quantum-state analysis, and small-scale unitary synthesis.

### 1.2 Supported capabilities

| Area | Current implementation |
| --- | --- |
| Quantum states | Normalized dense `C64` pure-state vectors |
| Circuits | Fixed gates, parameterized gates, custom unitaries, parameter sharing, and concatenation |
| Training | `Tensor` autograd and quantum adjoint differentiation |
| Observables | `PauliString` and real-coefficient `SparsePauliOp` |
| Analysis | Probabilities, marginals, fidelity, Bloch vectors, and terminal sampling |
| Losses and optimization | Rust supports `MSE`/`L1`/cross entropy, custom losses composed from operators, and `SGD`/`Adam` |
| User interfaces | Full Rust API; the Python native extension exposes common circuit, simulation, analysis, and training interfaces |

### 1.3 Current limitations

The current backend supports only dense `C64` pure-state vector simulation on CPUs. CUDA, sparse states, noise models, density matrices, quantum channels, mid-circuit measurement, and classical conditional control are not included. `BatchStateVectorSimulator` represents independent initial states sharing one circuit and its parameters; batch sampling is not currently available. Weight checkpoints save only circuit parameters, not circuit topology, optimizer state, training-data position, or random state. The Python API exposes a commonly used subset; use the native Rust API for full capabilities. Future support is defined by actual release notes.

## 2. Release package and environment setup

### 2.1 Release directory

Read [Release status and license](https://github.com/Quill-ArcLight/ArcQML#发行状态与许可证) before use or distribution. Original public source is for noncommercial use only; the official closed-source Runtime has a separate noncommercial binary license. Each component in the wheel retains its own license.

This release is an experimental Windows preview using Rust 1.98.0 and CPython 3.11; Linux acceptance is deferred. The Linux directories and commands below are historical environment examples and do not imply that this release includes Linux artifacts. Install the matching libraries and wheel supplied with this release, without mixing historical artifacts. See the [ArcQML repository](https://github.com/Quill-ArcLight/ArcQML) for current installation steps and [Security limitations](https://github.com/Quill-ArcLight/ArcQML/blob/HEAD/SECURITY.md) for known memory-safety issue S01.

All paths below are relative to the ArcQML release directory you obtained. Replace `PATH_TO_YOUR_FILES` with its actual location and preserve the relative directory structure inside the package.

```text
PATH_TO_YOUR_FILES/ArcQML/
├── crates/                 # Rust interface and supporting features
├── libs/
│   ├── x86_64-pc-windows-msvc/
│   │   └── arcqml_runtime_private.lib
│   └── x86_64-unknown-linux-gnu/
│       └── libarcqml_runtime_private.a
├── wheels/
│   └── arcqml-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
├── examples/               # Complete examples
├── docs/                   # Technical manual, API documentation, and tutorials
├── Cargo.toml
├── LICENSE-RUNTIME
└── ...
```

Binary libraries in `libs` must match the operating system and CPU architecture. Do not rename them or mix them with other ArcQML versions. The historical directory example above lists Rust Runtime paths for Windows `x86_64-pc-windows-msvc` and Linux `x86_64-unknown-linux-gnu`. Its Linux wheel is only for `CPython 3.11` on Linux `x86_64` and cannot be installed on Windows. Use the Windows Runtime and wheel supplied with this Windows preview.

Support for other operating systems, CPU architectures, and Python versions depends on the build artifacts actually provided in future releases.

### 2.2 Rust environment and Runtime configuration

The Rust interface uses `Rust 2024 edition`. The current stable toolchain is recommended; these examples were verified with Rust 1.98.0. Windows users can follow the [official Rust installation page](https://www.rust-lang.org/tools/install) to install `rustup`, select the MSVC toolchain, and install the corresponding Visual Studio C++ Build Tools and Windows SDK. Linux users can use the official installation command:

```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

After installation, run `rustc --version` and `cargo --version` to check availability. From the release root, set `ARCQML_RUNTIME_LIB_DIR` to the platform-matching subdirectory in `libs`. This variable selects the precompiled Runtime for linking.

Windows PowerShell:

```powershell
Set-Location "PATH_TO_YOUR_FILES\ArcQML"
$env:ARCQML_RUNTIME_LIB_DIR = (Resolve-Path ".\libs\x86_64-pc-windows-msvc").Path
cargo check -p arcqml
```

Linux:

```bash
cd PATH_TO_YOUR_FILES/ArcQML
export ARCQML_RUNTIME_LIB_DIR="$PWD/libs/x86_64-unknown-linux-gnu"
cargo check -p arcqml
```

The environment variable applies only to the current terminal session; set it again in new terminals. `cargo check` checks source and types, not final application linking.

Your own Rust project can reference the release interface through a path dependency:

```toml
[dependencies]
arcqml = { path = "PATH_TO_YOUR_FILES/ArcQML/crates/arcqml" }
```

Set `ARCQML_RUNTIME_LIB_DIR` before running that project as well. If you copy or move the release package, update both the dependency and Runtime paths.

### 2.3 Python environment and wheel installation

The following historical example targets Linux `x86_64`, `CPython 3.11`, and a manylinux2014-compatible glibc environment. It does not apply to this Windows preview. A separate Conda environment is recommended to avoid interference from existing Python or NumPy versions. See the [ArcQML repository](https://github.com/Quill-ArcLight/ArcQML) for current Windows commands. All historical commands below run from the corresponding release root:

```bash
cd PATH_TO_YOUR_FILES/ArcQML
conda create -n arcqml-example python=3.11 -y
conda activate arcqml-example
python -m pip install --upgrade pip
python -m pip install ./wheels/arcqml-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
```

The wheel statically embeds its matching ArcQML Runtime. Python users do not need `ARCQML_RUNTIME_LIB_DIR`, Rust, Maturin, or source builds. Verify import with `python -c "import arcqml; print(arcqml)"`. If pip reports an incompatible wheel, check `python --version`, `python -c "import platform; print(platform.machine())"`, and the operating system.

### 2.4 Data flow through a trainable forward and backward pass

```text
Circuit.parameters()
    │  Bind parameter values to Gates at execution time
    ▼
(Batch)StateVectorSimulator.run(circuit, observable)
    │  Interface validates and binds parameters; Runtime evolves the state; public Rust computes the expectation
    ▼
F64 scalar or [B] F64 Tensor
    │  Join ordinary Tensor losses in a single automatic-differentiation graph
    ▼
loss.backward()
    │  Classical VJP → Runtime quantum adjoint → Parameter.grad
    ▼
optimizer.step(circuit.parameters())
```

The entire quantum circuit enters the autograd graph as a custom `Tensor` operation. The public Rust layer validates parameters and shapes, evaluates observables, connects the graph, and updates the optimizer; the closed-source Runtime applies gates, executes circuit plans, and computes adjoint backpropagation. The parents of `run` include the initial state and circuit parameters, so external losses need not know how gates execute internally, and quantum expectations remain available to classical differentiable computation.

## 3. Mathematical, indexing, and storage conventions

### 3.1 Pure states and normalization

For `n` qubits, ArcQML represents a pure state as a complex vector of length `d`, where the state-space dimension is:

$$
d=2^n.
$$

The pure state and its normalization condition are:

$$
\lvert\psi\rangle
=\sum_{x=0}^{2^n-1}\alpha_x\lvert x\rangle,
\qquad
\sum_{x=0}^{2^n-1}\lvert\alpha_x\rvert^2=1.
$$

When importing a single state from `Tensor`, the framework checks the squared norm of the entire vector. Batch imports are checked row by row. To allow floating-point error, both interfaces require:

$$
\left\lvert
\sum_x\lvert\alpha_x\rvert^2-1
\right\rvert
\le 10^{-10}.
$$

The default constructor creates the all-zero computational basis state:

$$
\lvert 0\cdots 0\rangle.
$$

Amplitude 0 is 1; all other amplitudes are 0.

### 3.2 Qubit indices and binary-string display

ArcQML assigns `q0` to the least significant bit of the computational basis index, `q1` to the next bit, and so on. The relationship between basis states and numerical indices is:

$$
\lvert x\rangle
=\lvert q_{n-1}\cdots q_1q_0\rangle,
\qquad
x=\sum_{k=0}^{n-1}q_k2^k.
$$

Binary strings display qubits in descending order: `q[n-1]` on the left and `q0` on the right. In a two-qubit system, for example, internal index 1 is displayed as `01`.

| Two-qubit internal index | Binary display | `q1` value | `q0` value |
| --- | --- | --- | --- |
| 0 | 00 | 0 | 0 |
| 1 | 01 | 0 | 1 |
| 2 | 10 | 1 | 0 |
| 3 | 11 | 1 | 1 |

### 3.3 Local matrices and multi-qubit gate order

For multi-qubit gates, call arguments define the local basis order: `qubits[0]` is the least significant bit of the local matrix index, followed by `qubits[1]`. For controlled gates such as `cnot`, `cp`, `crx`, `cry`, and `crz`, the first qubit is the control and the second the target. Thus `cnot(0, 1)` uses `q0` as control and `q1` as target. Swap-like gates still require two valid, distinct qubit indices even though they are mathematically symmetric.

Public row-major data for a dense full unitary matrix is indexed first by the output computational basis index, then by the input index. Internally, full-unitary batch computation instead treats each computational basis input as a column-like batch state, then transposes to the public matrix layout.

## 4. Tensor, Parameter, and automatic differentiation

### 4.1 Tensor data and metadata

`Tensor` is the numerical object users manipulate directly. It combines `Storage`, a version counter, `TensorMeta`, and `AutogradMeta` in a shared handle. `TensorMeta` contains `shape`, `dtype`, `device`, `layout`, `strides`, and `offset`. Public queries include `shape`, `ndim`, `numel`, `dtype`, `device`, `layout`, `is_contiguous`, `strides`, and `offset`.

| `dtype` | Typical use | Can be an autograd leaf variable |
| --- | --- | --- |
| `F32` | Single-precision real parameters or data | Yes. |
| `F64` | Quantum parameters, expectations, losses, and default `zeros` | Yes. |
| `C64` | Double-precision complex values (two f64 values, 16 bytes), corresponding to `Complex64` / NumPy `complex128` | Yes. |
| `I64` | Integer labels or index data | No. |
| `Bool` | Boolean data | No. |

`Tensor::clone()` copies only the handle, sharing the underlying `Storage`, version counter, and autograd metadata. `deep_clone()` copies values, Tensor metadata, and existing gradients and preserves `requires_grad`, but creates a new leaf Tensor with independent storage and version counter, without inheriting the original graph. It is not a differentiable copy operator. `storage_mut()` acquires a write lock and increments the shared version counter when its `guard` is released. This counter detects in-place changes to forward data before backpropagation.

### 4.2 Autograd graphs and gradient lifetimes

Only leaf `Tensor` objects can enable `requires_grad`; differentiable leaf `dtype` values are limited to `F32`, `F64`, and `C64`. Differentiable operations record their graph when gradient recording is enabled in the current thread and at least one parent requires gradients. Only leaf gradients persist by default; call `retain_grad()` explicitly to keep gradients on intermediate `Tensor` objects.

`backward()` automatically supplies upstream gradient 1 only for scalar outputs with `shape=[]`. Nonscalar outputs require `backward_with_grad(gradient)`, with a gradient compatible in shape, `dtype`, and other requirements. Default `backward` frees the traversed graph nodes. To reuse the graph, use `backward_with_grad_retain_graph` from the first backward pass; this cannot be repaired after freeing it. `retain_grad()` retains intermediate gradients, not the graph. Gradients accumulate, so training iterations must explicitly call `zero_grad`. Prefer combining losses sharing a forward pass into one scalar before backpropagation.

### 4.3 Parameter semantics

`Parameter` is a semantic wrapper around `Tensor` and the main numerical object for trainable parameters. `Parameter::new` requires a leaf `Tensor` with `dtype` `F32`, `F64`, or `C64`, sets `requires_grad` to `true`, and defaults to `trainable` to `true`. Invalid inputs panic; use `Parameter::try_new` for recoverable errors. `Circuit` parameter registration requires `numel=1` and `F32`/`F64`; use scalar parameters with `shape=[]` for training. Quantum gate-angle training does not accept `C64` parameters.

`freeze()` sets `trainable` to `false`, causing optimizers to skip the parameter. It does not disable `requires_grad` on the underlying `Tensor`, which may still receive gradients. `unfreeze()` restores `trainable=true` and enables gradient recording on the underlying `Tensor`. Thus `requires_grad` controls backpropagation participation, while `trainable` controls optimizer updates.

## 5. Circuit, Operation, and parameter management

### 5.1 Circuit structure

`Circuit` stores `num_qubits`, an execution-ordered list of `Operation` objects, and a `Parameter` list. `add_gate` wraps a `Gate` and `qubit` list into an `Operation`. Only when the simulator is called does the interface validate and bind parameters, after which ArcQML Runtime applies the `Gate` to the current quantum state.

`Circuit::depth()` computes parallel depth from qubit resources: each `Operation` receives one plus the maximum current layer of its qubits, and writes that layer back to all participating qubits.

### 5.2 Built-in gates

| Category | Rust `Circuit` methods | Parameter behavior |
| --- | --- | --- |
| Fixed single-qubit | `i`, `x`, `y`, `z`, `h`, `s`, `sdg`, `t`, `tdg`, `sx`, `sxdg` | No trainable parameters. |
| Parameterized single-qubit | `rx`, `ry`, `rz`, `phase`, `u1`, `u2`, `u3` | Create new parameters by default; some provide `_fixed` or `_param` variants. |
| Controlled | `cnot`/`cx`, `cy`, `cz`, `ch`, `cs`, `ct`, `cp`, `crx`, `cry`, `crz` | The first two `qubit` arguments are control and target, respectively. |
| Two-/multi-qubit | `swap`, `iswap`, `dcx`, `ecr`, `rxx`, `ryy`, `rzz`, `rzx`, `fsim`, `toffoli`, `cswap`, `mcx` | Rotation gates and `fSim` are trainable; the rest are fixed. |
| Extensions | `Gate::custom_unitary(name, arity, matrix)` | Accepts a `row-major` `C64` matrix; dimensions and unitarity are checked at construction. |

Python `Circuit` currently exposes `h`, `x`, `y`, `z`, `rx`, `ry`, `rz`, `phase`, `u3`, and `cnot`. Its gate set is smaller than the Rust `Circuit` set; use Rust for other controlled gates, multi-qubit gates, or custom unitaries.

### 5.3 Three ways to add gate parameters

| Method | Example | Parameter-table behavior | Suitable use |
| --- | --- | --- | --- |
| Automatic registration | `circuit.ry(0.3, 0)` | Creates an `F64` scalar `Parameter` with a stable automatic name | Ordinary trainable gates. |
| Fixed parameter | `circuit.ry_fixed(0.3, 0)` | Uses `Gateparam::Fixed`; registers no `Parameter` | Encoding constants and nontrainable layers. |
| Explicit sharing | `id = add_parameter(...)`; `ry_param(id, 0)` | Multiple gates reference one `ParameterId` | Symmetric ansatzes and shared weights. |

Automatic `gate`-parameter names combine the gate, role, qubit, and parameter-table index, such as `ry_q0_theta_0`. Manual `add_parameter_tensor` calls default to `parameter_<index>`. Names are part of `checkpoint` matching, so changing model-construction order or gate naming rules affects compatibility for persistently saved models.

### 5.4 Shared parameters and gradient accumulation

```rust
use arcqml::prelude::*;

let mut circuit = Circuit::new(1)?;
let theta = circuit.add_parameter(0.3)?;
circuit.ry_param(theta, 0usize)?;
circuit.rz_param(theta, 0usize)?;
// During backpropagation, add contributions to the same theta from both operations.
```

Each parameterized `Operation` stores only a `ParameterId`. Before execution, the interface reads current `Parameter` `Tensor` values from `Circuit` and passes them to Runtime. Optimizer updates therefore require no circuit reconstruction: the next run uses the new angles automatically. Adjoint backpropagation also accumulates contributions from multiple gates by `ParameterId`, implementing shared-parameter gradient accumulation.

### 5.5 Validation, copying, and concatenation

`validate()` checks that the `qubit` count is nonzero, `Operation` qubits are valid, `Gate` parameter references belong to the current table, and parameter names are nonempty and unique. `Circuit::clone()` calls `deep_clone()`, so the copied `Circuit` shares neither parameter values nor gradients with the original.

`append(right)` consumes the right circuit, appends its operations, and remaps unbound parameters. `append_with_bindings(right, bindings)` explicitly binds selected right-side `ParameterId` values to existing left-side parameters. Both circuits must have the same qubit count, and successful concatenation clears parameter gradients in the result.

### 5.6 Current limits on parameter expressions

Parameterized gates currently store only a fixed scalar or a single `ParameterId` in each parameter slot, not expressions involving multiple parameters. Expressions such as `-x` and `x + y` cannot be supplied as gate parameters directly. Multiple gates may share a `ParameterId`, but arithmetic relationships between parameters cannot be declared inside `Circuit`.

For parameter expressions, currently compute the value outside the circuit and insert it as a fixed or independent parameter. This does not automatically preserve dependencies on the original parameters: registering the result of `x + y` as a new `Parameter` yields a gradient only for that new parameter, without automatically applying the chain rule to `x` and `y`. Recompute and update the value before each training forward pass.

The current version does not support registering a nonleaf Tensor expression directly as a gate parameter; such a call may also panic. Training the original expression variables requires a caller-provided parameter-mapping VJP / chain-rule gradient. Rewriting values each iteration does not create dependencies automatically. Prefer single `ParameterId` values and parameter sharing for ordinary training. Connecting quantum outputs to classical differentiable losses is supported.

## 6. Pauli observables and Hamiltonians

### 6.1 PauliString

`PauliString` represents a tensor product of `PauliOp` values, storing only non-`I` terms in `qubit`-index order. Construction checks `qubit` ranges and duplicate `qubit`s. `identity`, `single`, `x`, `y`, and `z` provide shortcuts. Two `PauliString` objects commute when the number of positions with different, non-`I` Paulis on the same qubit is even.

$$
P=P_{n-1}\otimes\cdots\otimes P_1\otimes P_0,
\qquad
P_q\in\{I,X,Y,Z\}.
$$

For two qubits, `X(0)` corresponds to `I ⊗ X`, mapping `|00⟩` to `|01⟩`. `multiply(rhs)` returns (complex phase, resulting `PauliString`). This supports Pauli algebra; public `SparsePauliOp` coefficients are restricted to finite real values, so observable expectations are real.

### 6.2 SparsePauliOp and compilation caching

`SparsePauliOp` is a collection of `PauliTerm` objects, each `term` containing a finite real `coefficient` and a `PauliString`. `Hamiltonian`, `PauliSum`, and `PauliObservable` are aliases. Construction requires nonzero `num_qubits` and consistent qubit counts across terms. `scale` and `simplify` also reject nonfinite factors or invalid `tolerance` values.

$$
H=\sum_j c_jP_j,
\qquad
c_j\in\mathbb{R}.
$$

At the first state-vector evaluation, the public Rust layer prepares a reusable execution representation of `SparsePauliOp`: diagonal `term`s can be merged, while off-diagonal Pauli strings become bit masks for expectation and `H|ψ⟩` evaluation in the public observable implementation. Adding a term invalidates the cache. `clone` copies the observable definition but not its execution cache. Reusing one `Hamiltonian` during training therefore avoids preparing the same execution information each iteration.

```rust
let mut h = SparsePauliOp::single(2, 0usize, Pauli::Z, 0.5)?;
h.add_pauli_string(-1.0, PauliString::x(2, 1usize)?)?;
// H = 0.5 Z(0) − 1.0 X(1)
```

### 6.3 Expectation semantics

For the current pure state, single-state `expectation_observable` returns an `F64` scalar `Tensor`. The batch version returns a `Tensor` with `shape` equal to `[B]` and dtype `F64`, one value per initial-state row. The expectation is:

$$
\langle H\rangle
=\operatorname{Re}\!\left(
\langle\psi\rvert H\lvert\psi\rangle
\right).
$$

The differentiable path created by `run` is reused for adjoint backpropagation, avoiding reconstruction of the same observable-action result.

## 7. Single and batched state-vector simulation

### 7.1 StateVectorSimulator: stateful and stateless interfaces

| Interface | State effect | Returns | Main use |
| --- | --- | --- | --- |
| `new` / `from_state_tensor` | Creates the initial state | Simulator | Default zero state or imported normalized custom state. |
| `apply_gate` / `apply_operation` | Changes current `state` | `&mut Self` | Stepwise execution of fixed-parameter `Gate`/`Operation`. |
| `apply_circuit` | Changes current `state` | `&mut Self` | Read current `Circuit` parameters and execute gate by gate. |
| `run` | Leaves current `state` unchanged | Differentiable `F64` scalar `Tensor` | Training and functional forward passes. |
| `amplitudes` | The method itself does not change state | Shared handle to the current `C64` Tensor | Read amplitudes while retaining existing graph connections. |
| `sample_counts` | Read-only | `BTreeMap<String, usize>` | Terminal sampling of all qubits in the current state. |

`apply_circuit` and `run` have different semantics. The former changes the simulator `state`, so repeated calls continue evolving the already evolved `state`. The latter copies the current caller state into a forward buffer and returns an expectation without changing `self`. To repeatedly execute from the zero state, use a new simulator or call `reset` before `apply_circuit`. Training loops normally use `run`.

The shared Tensor returned by Rust `amplitudes()` is not an independent numerical copy. In-place writes to its storage affect the original state and may violate normalization or version constraints of an existing graph. Python `numpy()` instead produces an independent array copy.

### 7.2 Importing and validating initial states

Single-state `from_state_tensor` accepts a contiguous, CPU, `Dense`, one-dimensional `C64` `Tensor` with `shape` exactly `[d]`. The batch version requires `[B, d]`, a nonzero batch size, and rowwise normalization. Here `d` is the state-space dimension; both interfaces use the normalization tolerance specified in that section.

For NumPy imports, a single state requires a C-contiguous one-dimensional `complex128` array, while a batch requires a C-contiguous two-dimensional `complex128` array. The binding first checks Python dimensions, copies to a `FlatC64` `Tensor`, and lets the Rust simulator perform the same normalization and layout validation.

### 7.3 BatchStateVectorSimulator

`BatchStateVectorSimulator` stores `state` with `shape` `[batch_size, d]`, one independent pure state per row. `apply_circuit` applies the same circuit to each row; `run` returns an `F64` `Tensor` of shape `[batch_size]`. Runtime interprets states in row-major order. With the `parallel` feature enabled, the public Rust layer uses Rayon for outer batch scheduling, so thread count, batch size, state dimension, and memory bandwidth all affect performance.

**Meaning of batch**  A batch is a collection of independent initial states sharing one `Circuit` and one `SparsePauliOp`, not parallel branches within one quantum state. It cannot represent different gate topologies or qubit counts for individual samples. Batch execution is useful when adapting classical machine-learning tasks with larger datasets.

## 8. Quantum adjoint differentiation

### 8.1 Forward definition

For gates in execution order, ArcQML `run` evolves the initial state gate by gate, then evaluates the `Hamiltonian` expectation. The state after gate `k` and the final objective are defined as:

$$
\lvert\psi_k\rangle
=U_k\lvert\psi_{k-1}\rangle,
\qquad
f=\operatorname{Re}\!\left(
\langle\psi_m\rvert H\lvert\psi_m\rangle
\right).
$$

If the initial state or any `Circuit` `Parameter` requires gradients and the current thread records them, `run` saves the final state, the `Hamiltonian` applied to that state, bound operations, and parameter-slot mappings, and creates a custom `F64` `Tensor` autograd node. Otherwise it executes only the forward pass without saving adjoint context.

### 8.2 Single-state adjoint backward formula

Let the outer real scalar loss be `L`, the single-state expectation be `f`, and the scalar upstream gradient be `g = ∂L/∂f`. To avoid counting the upstream weight twice, the formulas below use an unscaled adjoint state:

$$
\lvert\lambda_m\rangle=H\lvert\psi_m\rangle,
\qquad
\lvert\lambda_{k-1}\rangle=U_k^\dagger\lvert\lambda_k\rangle.
$$

Applying conjugate-transposed gates in reverse order recovers each gate input state. When parameter `p` occurs in several gates, its gradient sums their contributions:

$$
\frac{\partial L}{\partial p}
=2g\sum_{k:\,p\text{ occurs in }U_k}
\operatorname{Re}\!\left[
\langle\lambda_k\rvert
\frac{\partial U_k}{\partial p}
\lvert\psi_{k-1}\rangle
\right].
$$

The implementation may multiply `g` into the adjoint buffer at the start of the backward pass; it must then not multiply by `g` again. If the initial state requires gradients, ArcQML uses the complex-gradient convention `∂L/∂Re(ψ₀) + i ∂L/∂Im(ψ₀)`:

$$
\nabla_{\psi_0}L=2g\lvert\lambda_0\rangle.
$$

This is twice the conjugate Wirtinger derivative and should not be confused with the derivative without the factor of 2.

Parameter gradients are scalar `Tensor` objects with the corresponding `Parameter` `dtype`, `F32` or `F64`. Only the final state and bound-gate information are needed, not the complete state at every layer.

### 8.3 Batch upstream gradients and VJPs

Batch `run` produces a `[B]` vector, not a scalar, so it cannot directly use argument-free `backward()`. Usually reduce it to a scalar with a classical loss such as `mse_loss` or binary cross entropy with `logits`. When the outer graph passes `grad_output`, the batch adjoint node requires an `F64` `Tensor` with `shape` `[B]` and multiplies each upstream gradient into the corresponding row of `adjoint_state`.

Batch mode therefore computes a vector-Jacobian product (VJP): the external classical network or loss determines upstream weights for sample outputs, and the quantum adjoint propagates them to shared `Circuit` parameters and differentiable initial states.

### 8.4 no_grad and training-graph memory

Wrapping `run` in `no_grad()` or Python `with arcqml.no_grad():` sets `records_gradients` to `false`, retains no adjoint context, and returns an ordinary forward `Tensor`. Use this mode for validation, inference, benchmarking, and read-only state analysis. If backpropagation is needed, do not first convert predictions to `f64` or NumPy arrays, which leaves the `Tensor` graph.

Rust must bind the scope guard; calling `no_grad();` alone immediately restores recording at the end of the statement:

```rust
{
    let _guard = no_grad();
    let prediction = simulator.run(&circuit, &observable)?;
    // Read prediction in this scope.
}
```

Gradient recording is thread-local. In Python, create a fresh context each time with `with arcqml.no_grad():`; do not cache or reenter the same context object.

## 9. State analysis and terminal sampling

### 9.1 Exact and marginal probabilities

`analysis::probabilities` reads current `amplitudes` and computes `square(abs(amplitudes))`. It returns a differentiable `F64` `Tensor` with `shape` `[d]`; entry `index` corresponds to the same computational basis index, whose least significant bit is `q0`.

`marginal_probabilities` validates that the selection is nonempty, unique, and in range, then sorts selected `qubit`s in descending order. For each full basis index, selected bits form an output `segment id`, and `segment_sum` aggregates their probabilities. Selecting `[0, 2]` gives `q2q0` order: `00`, `01`, `10`, `11`, regardless of the input selection order.

### 9.2 Pure-state fidelity and Bloch vectors

`fidelity(lhs, rhs)` first checks equal `simulator` qubit counts, then computes the squared modulus of the state inner product:

$$
F\!\left(\lvert\psi\rangle,\lvert\phi\rangle\right)
=\left\lvert\langle\psi\vert\phi\rangle\right\rvert^2.
$$

The result is not clamped, preserving agreement between forward expressions and backward rules. Identical normalized pure states theoretically give 1 and orthogonal states 0, but floating-point rounding may produce tiny out-of-range deviations.

`bloch_vector` computes coherence terms for each pair of zero-branch and one-branch amplitudes of the specified `qubit`, accumulating three components as follows:

$$
\begin{aligned}
r_x&=2\operatorname{Re}\!\left(\sum a_0^*a_1\right),\\
r_y&=2\operatorname{Im}\!\left(\sum a_0^*a_1\right),\\
r_z&=\sum\left(\lvert a_0\rvert^2-\lvert a_1\rvert^2\right).
\end{aligned}
$$

It returns an ordinary `BlochVector {x, y, z}`, not a differentiable `Tensor` in the current implementation.

### 9.3 Exact behavior of sample_counts

`sample_counts` is available only on the single-`state` simulator. It requires positive integer `shots`, reads the current normalized `C64` `state`, uses amplitude `norm_sqr` values as a discrete distribution, initializes `StdRng` from the supplied `seed` or a fresh random `seed`, and draws that many independent samples. It does not execute a `Circuit`, write back to state, cause measurement collapse, or create a differentiable node.

```rust
let mut sim = StateVectorSimulator::new(2)?;
sim.apply_circuit(&bell)?;
let counts = sim.sample_counts(1_000, Some(42))?;
// Display keys in q1q0 order, such as "00" and "11".
```

## 10. Loss functions, optimizers, and training loops

### 10.1 Losses: input constraints and forward formulas

| Function | Key constraints |
| --- | --- |
| `mse_loss` | Nonempty `prediction` and `target`, same dtype (`F32`/`F64`), broadcast-compatible shapes. |
| `l1_loss` | Nonempty `prediction` and `target`, same dtype (`F32`/`F64`), broadcast-compatible shapes. |
| `binary_nll_loss` | One-dimensional `prediction` and `labels` of equal length; labels must be 0 or 1. |
| `BCE with logits` | `logits` and `targets` have identical `shape` and `dtype`; `target` values lie between 0 and 1. |
| `cross_entropy_loss` | A two-dimensional `logits` `Tensor`; the `labels` count equals batch size, and every label is a valid class index. |

These losses use the current CPU Dense numerical operators. Let `N` be the element count of the broadcasted error Tensor, with the `i`th broadcasted elements of `prediction` and `target` denoting predicted and target values. For a `[B, C]` error tensor, for example, `N = B × C`. Mean squared error is:

$$
L_{\mathrm{MSE}}
=\frac{1}{2N}\sum_{i=1}^{N}
\left(\hat{y}_i-y_i\right)^2.
$$

The `L1` loss is:

$$
L_{\mathrm{L1}}
=\frac{1}{N}\sum_{i=1}^{N}
\left\lvert\hat{y}_i-y_i\right\rvert.
$$

In `binary_nll_loss`, `prediction` is a Pauli-Z expectation, not an ordinary probability or `logit`. The framework first clamps the expectation:

$$
z_i^{\mathrm{clip}}
=\operatorname{clamp}\!\left(
z_i,-1+2\varepsilon,1-2\varepsilon
\right),
\qquad
\varepsilon=10^{-12}.
$$

The clamped expectation is then converted to probabilities for two classes:

$$
p_i(0)=\frac{1+z_i^{\mathrm{clip}}}{2},
\qquad
p_i(1)=\frac{1-z_i^{\mathrm{clip}}}{2}.
$$

The corresponding binary negative log-likelihood is:

$$
L_{\mathrm{binary\ NLL}}
=-\frac{1}{N}\sum_{i=1}^{N}
\left[
(1-y_i)\log p_i(0)+y_i\log p_i(1)
\right],
\qquad
y_i\in\{0,1\}.
$$

Binary cross entropy taking `logits` directly uses a numerically stable form, where `N` is the total number of logits:

$$
L_{\mathrm{BCE}}
=\frac{1}{N}\sum_{i=1}^{N}
\left[
\max(x_i,0)+\log\!\left(1+e^{-\lvert x_i\rvert}\right)-y_ix_i
\right].
$$

For multiclass `logits` with B samples and C classes, cross entropy is:

$$
L_{\mathrm{CE}}
=-\frac{1}{B}\sum_{b=1}^{B}
\log\!\left[
\operatorname{softmax}(X_b)_{y_b}
\right].
$$

### 10.2 Optimizers

#### SGD: updates with coupled L2 regularization

`Sgd::step` updates each `Parameter` that is `trainable`, has `requires_grad` enabled, and has a `grad`. For `F32` or `F64`, each element is updated as:

$$
\theta\leftarrow\theta-\eta\left(g+\lambda\theta\right).
$$

$$
\eta=\text{learning rate},
\qquad
g=\text{current gradient},
\qquad
\lambda=\text{weight decay coefficient}.
$$

Parameters without gradients count toward `skipped_no_grad`; frozen parameters or those with `requires_grad=false` count toward `skipped_frozen`.

#### Adam: bias correction and weight decay placement

At the first `step`, `Adam` creates state slots by position in the parameter slice. Later calls must preserve parameter count and order. It stores the step count, first moment, and second moment. Each update first adds the coupled `L2` term to the current gradient:

$$
\widetilde{g}_t
=g_t+\lambda\theta_{t-1}.
$$

Then it updates the first and second moments:

$$
\begin{aligned}
m_t&=\beta_1m_{t-1}+(1-\beta_1)\widetilde{g}_t,\\
v_t&=\beta_2v_{t-1}+(1-\beta_2)\widetilde{g}_t^2.
\end{aligned}
$$

After bias correction, it updates the parameters:

$$
\begin{aligned}
\widehat{m}_t&=\frac{m_t}{1-\beta_1^t},\\
\widehat{v}_t&=\frac{v_t}{1-\beta_2^t},\\
\theta_t&=\theta_{t-1}
-\eta\frac{\widehat{m}_t}{\sqrt{\widehat{v}_t}+\varepsilon}.
\end{aligned}
$$

Here `weight_decay` therefore enters the moment estimates and is not the decoupled weight decay used by `AdamW`.

Constructors require finite, nonnegative `learning_rate` and `weight_decay`, `beta1` and `beta2` in `[0, 1)`, and finite positive `epsilon`. Reuse the same `Adam` instance during training; recreating it loses momentum and step counts.

### 10.3 Recommended training loop

```rust
let mut optimizer = Adam::new(0.01, 0.9, 0.999, 1e-8, 0.0)?;
for _ in 0..steps {
    let prediction = simulator.run(&circuit, &observable)?;
    let loss = mse_loss(&prediction, &target)?;
    loss.backward()?;
    optimizer.step(circuit.parameters())?;
    optimizer.zero_grad(circuit.parameters());
}
```

Call `zero_grad` after each `optimizer.step` to clear current gradients and prevent accidental accumulation in the next iteration. Do not replace or modify a forward-pass `Parameter` `Tensor` in place before `backward`, or version checks may report modified forward data. Let `optimizer.step` perform updates.

### 10.4 Custom losses

Rust users can connect quantum outputs to differentiable operators such as `sub`, `square`, `mul`, and `mean` to build scalar losses. Ordinary operator composition requires no handwritten backward pass. To specify derivatives for out-of-graph computation or a complete expression, use `CustomOp` and `apply_custom_op`. Autograd follows local backward rules in the actual graph; it does not symbolically simplify arbitrary equivalent mathematical expressions.

See the [Custom loss tutorial](tutorial/custom_loss.md) and [Custom loss example](../rust/custom_loss.rs) for full input contracts, weighted MSE, batch circuit training, analytical/numerical gradient checks, and custom backward examples. Python does not currently export the corresponding general operator-composition or `CustomOp` interfaces.

## 11. Weight checkpoints and full-unitary fitting

### 11.1 Weight checkpoint file contract

`save_weights` writes formatted JSON with fixed `format` `arcqml/weights`. Each parameter records `name`, `dtype`, and `value`; records are sorted by name for stable ordering of identical states. Only `Circuit` `Parameter` objects with `numel=1` and dtype `F32`/`F64` can be written.

```json
{
  "format": "arcqml/weights",
  "num_qubits": 2,
  "parameters": [
    {"name": "ry_q0_theta_0", "dtype": "f64", "value": 0.36154268969437336}
  ]
}
```

`load_weights` first validates `format`, qubit count, parameter-name set, duplicate names, `dtype`, and finite `value`s parsed from JSON, and prepares replacement tensors. It then writes parameters and clears gradients. It does not compare gate topology, so the caller must reconstruct the expected isomorphic `Circuit`.

### 11.2 DenseUnitary data contract

`DenseUnitary` stores `num_qubits`, `dimension`, and `row-major` `C64` data. The `dimension` must be a power of 2; public data is indexed by output basis index first, then input index. `DenseUnitary::from_tensor` checks only `C64`, a two-dimensional square shape, length after contiguous copying, and finiteness. Public `unitary_from_tensor` additionally validates unitarity using these default tolerances:

$$
\varepsilon_{\mathrm{unitary}}=10^{-10}.
$$

`unitary_from_circuit` executes the `Circuit` on an internal batch of all computational basis initial states, then transposes to the public `row-major` layout. For `n` qubits, space complexity is:

$$
O(4^n).
$$

One matrix copy occupies about `16 × 4^n` bytes, and internal computation needs additional buffers. This feature therefore suits small-scale circuit comparison and synthesis.

### 11.3 Unitary fidelity, loss, and analytical gradients

The trace overlap, full-unitary fidelity, and loss for a target unitary and the current circuit unitary are:

$$
\begin{aligned}
s&=\operatorname{Tr}\!\left(U_{\mathrm{target}}^{\dagger}U(\boldsymbol{\theta})\right),\\
F_U&=\operatorname{clamp}\!\left(\frac{\lvert s\rvert^2}{d^2},0,1\right),\\
L_U&=1-F_U.
\end{aligned}
$$

Unlike the pure-state method in Chapter 9 (`fidelity`), this explicitly clamps to the interval from 0 to 1 to contain out-of-range rounding errors.

`unitary_loss` returns an `F64` scalar compatible with the shared autograd system. Its backward pass restores the forward `StateBatch` in reverse order, accumulates parameter gradients from the analytical unclamped objective, and multiplies by the outer scalar upstream gradient:

$$
\frac{\partial L_U}{\partial p}
=-\frac{2}{d^2}
\operatorname{Re}\!\left(
s^*\frac{\partial s}{\partial p}
\right).
$$

The gradient implementation does not additionally zero gradients outside the forward clamping interval. For exact unitaries, unclamped fidelity is theoretically at most 1. Using a `DenseUnitary` target without validating unitarity may produce a clamped loss inconsistent with this gradient. `unitary_loss` itself checks only qubit-count compatibility with the circuit. Validate targets with `unitary_from_tensor` or explicit `validate`. Small rounding excursions must still be interpreted according to these implementation semantics.

## 12. Visualization, Python bindings, and parallel execution

### 12.1 Text and SVG circuit diagrams

The top-level Rust `draw` interface only renders a `Circuit` as a Unicode string: `q0` appears at the top, single-qubit gates as boxes, and controlled gates as control points and connecting lines. Parameterized gates show `p<index>` or a fixed value.

```rust
let mut circuit = Circuit::new(3)?;
circuit.h(0usize)?.cnot(0usize, 1usize)?.rz(0.3, 2usize)?;
println!("depth = {}", circuit.depth());
println!("{}", draw(&circuit));
```

`arcqml::visualization::draw_svg(&circuit)` returns an SVG string. `write_svg(&circuit, path)` writes a file and returns `Result`; callers should handle write errors:

```rust
arcqml::visualization::write_svg(&circuit, "circuit.svg")?;
```

### 12.2 Current Python exports

| Python object / function | Rust counterpart | Notes |
| --- | --- | --- |
| `Tensor` / `tensor` / `no_grad` | `arcqml::core::Tensor` and gradient mode | `numpy()` and `item()` explicitly leave the `Tensor` representation. |
| `Circuit` | `arcqml::circuit::Circuit` | Only common gates are bound; parameter names, values, and gradients are returned as a `dict`. |
| `StateVectorSimulator` | Single-state simulator | `from_amplitudes` requires a C-contiguous one-dimensional `complex128` NumPy array. |
| `BatchStateVectorSimulator` | Batch simulator | `from_amplitudes` requires a C-contiguous two-dimensional row-major `complex128` array. |
| `PauliSum` | `SparsePauliOp` | `observable` supplied to `run`. |
| `mse_loss` / `BCE logits` | Subset of Rust loss interfaces | Python does not currently export `L1`, `binary_nll`, or multiclass `CE`. |
| `Adam` | `arcqml::optim::Adam` | Defaults: `beta1=0.9`, `beta2=0.999`, `epsilon=1e-8`, `weight_decay=0`. |

Python `simulator.run` leaves the current state unchanged; `apply_circuit` changes it, matching Rust semantics. Python `BatchStateVectorSimulator` also lacks `sample_counts`. Python `Circuit.gradients()` reports an error according to the binding implementation if any parameter has no gradient yet, so call `backward` before reading gradients during training.

### 12.3 Rayon thread pool

With the default `parallel` feature enabled, the top-level Rust API provides `init_rayon(num_threads)` and `rayon_num_threads()`, and Python exports both as well. Initialize the global Rayon worker pool before the first parallel computation. Batch execution schedules work by sample, so thread count, batch size, state dimension, and memory bandwidth all affect performance. The pool can be initialized only once; configure it before the first simulation.
