This document provides guidance for AI agents working with the Quax codebase.
Quax is a high-performance quantum information science library built on top of JAX. It provides tools for:
- Quantum state manipulation (state vectors and density matrices)
- Quantum gates and unitary operations
- Superoperator representations (Kraus, Choi, Pauli-Liouville, SuperOp)
- Quantum channels (depolarizing, thermal relaxation, bit flip, etc.)
- Distance metrics and fidelity measures
- Quantum operator transformations and compositions
- Python: 3.12+
- Core Framework: JAX (for automatic differentiation and GPU acceleration)
- Testing: pytest
- Linting: ruff, pyright
- Documentation: Sphinx with Furo theme
- Build System: poetry-core
src/quax/ # Main package
├── __init__.py # Public API exports
├── gates.py # Common quantum gates and measurement instruments (public submodule)
├── states.py # Predefined states (public submodule)
├── ensembles.py # Predefined ensembles (public submodule)
├── _apply.py # Applying operators to states
├── _apply_superoperator.py # Superoperator application logic
├── channels.py # SuperOp channels + measurement instrument constructors (public submodule)
├── _compose.py # Operator composition
├── _metrics.py # Fidelity and distance functions
├── _mul.py # Scalar multiplication logic
├── _observables.py # Observable utilities
├── _operator_basis.py # Operator basis construction
├── _power.py # Operator exponent operations
├── _promotion.py # State promotion utilities
├── _quantum_objects.py # Core quantum types (State, Operator, QuantumInstrument, etc.)
├── _random.py # Random quantum objects
├── _state.py # State creation and manipulation
├── _superoperator_transformations.py # Convert between representations
├── _tensor.py # Tensor product operations
├── _validation.py # Validation utilities
└── _visualization.py # Plotting functions
tests/ # Test suite
├── test_*.py # Unit tests for each module
├── instrument_helpers.py # Shared helpers for instrument tests
├── reference_pauli_liouville.py # Reference implementations
└── conftest.py # Pytest configuration
docs/ # Sphinx documentation
└── api/ # API reference
Most public functions are exported from the top level of the package. However, gates, states, and ensembles are organized into public submodules:
- gates: Access quantum gates via
qx.gates.X,qx.gates.CNOT,qx.gates.MEASURE(), etc. - states: Access predefined states via
qx.states.KET0,qx.states.XPLUS, etc.
The private submodules (those starting with an underscore) are organized for development convenience rather than providing a subpackage architecture.
# From the project root...
# Install dependencies
poetry installThe makefile contains recipes for common development tasks. For example
Note that tests should always be run with jax at 64bit precision. This can be done by settings the environment variable
JAX_ENABLE_X64=1Run the tests with
make test-packageTests are run with pytest and each file typically has a corresponding test file.
# Run all tests
poetry run pytest
# Run specific test file
poetry run pytest tests/test_apply.py
# Run with verbose output
pytest -vmake test-examplesmake formatmake check-typesmake check-formatcd docs
make html- Line Length: 120 characters max
- Type Hints: Required for all public functions
- Imports: JAX imports should use
import jaxandimport jax.numpy as jnp. In-package imports should be relative ex.from ._quantum_objects import UnitaryIn tests or example notebooks, always useimport quax as qxand then acccess functions usingqx.function. - Docstrings: Required for all public functions, use ReST format
A function that accepts several quantum types dispatches on the type of its first argument with
functools.singledispatch, rather than branching on isinstance or taking a union of input types.
The base function carries the docstring and raises a TypeError naming the accepted types;
each type gets its own registered _-prefixed implementation. estimate, to_choi, promote,
mul and to_pauli_vector all follow this pattern.
@singledispatch
def to_pauli_vector(operator) -> Array:
"""The coefficients of a state or observable in the Hermitian operator basis."""
raise TypeError(f"to_pauli_vector() does not support type {type(operator)!r}. ...")
@to_pauli_vector.register(DensityMatrix)
def _density_matrix_to_pauli_vector(operator: DensityMatrix) -> Array: ...This keeps each type's code path short and separately testable, and lets a new quantum type opt in
by registering rather than by editing an if/elif chain. A union type hint
(DensityMatrix | StateVector | Observable) with a body that branches on which one it got is the
shape to avoid.
Alternative constructors — from_matrix, from_pauli_vector, from_bloch_vector,
from_superop — are @classmethods that build through cls(...), so they are annotated
-> Self (from typing), never with the quoted name of the class they are defined on. A quoted
name lies about a subclass: Involution.from_matrix(...) really returns an Involution, and
Self is what tells a type checker so.
- Functions:
snake_case(e.g.,apply_unitary_to_state_vector) - Classes:
PascalCase(e.g.,StateVector,DensityMatrix) - Constants:
UPPER_CASE(e.g.,KRAUS_OPS) - Modules:
snake_case(e.g._pauli_vector.py)
- Internal implementation modules start with
_(e.g._pauli_vector.py) - A private helper used only inside its own module starts with
_(e.g._shifted_flat) - A private helper, that is defined in an internal module and used outside that module, does not
start with a
_(e.g.weyl_coefficientsin_fast_weyl_transforms.py,eigenvalue_tolerancein_tolerance.py) - A private helper defined in a public module always starts with a
_
- Pure Functions: All functions should be JAX-compatible (pure, no side effects)
- Array Operations: Use
jax.numpyinstead ofnumpy - JIT Compilation: Functions should be compatible with
jax.jit, and public array-valued functions should carry the decorator rather than leave jitting to the caller. - PRNG Keys: Use
jax.random.PRNGKeyfor randomness. Use the more modernjax.random.key(seed)for initializing keys. - Shape Handling: Use
dimstuples to specify system dimensions
jax.jit takes its options directly as a decorator — do not wrap it in functools.partial:
@jax.jit(static_argnames=("power",))
def integer_power_superop(superop: SuperOp, power: int) -> SuperOp: ...not @partial(jax.jit, static_argnames=("power",)). An argument is static when it selects the
computation rather than feeding it: a shape, a dimension, an iteration cap, a flag that picks
between two solves, or an exponent that jnp.linalg.matrix_power needs concretely.
A quantum object is a pytree, so its Python type is part of the tracer's structure: a function may
branch on isinstance(channel, Choi) or dispatch on the type and still be jitted, and it can return
a different representation for a different input type. What it cannot do is branch on an array
value; a data-dependent loop (lax.while_loop) is jittable but not reverse-mode differentiable.
A central task of the package is tracking the qudit dimensions. States have a single dimension for each qudit, while operators have both input and output dimensions which may sometimes differ (for example if a qutrit is promoted to a quart). The dimensions are represented as tuple of integers, for example (2, 2) for a 2-qubit state or ((2, 2), (2, 2)) for 2-qubit operator. Operator dimensions are (d_out, d_in).
Qubit: 2-dimensional qubit Qutrit: 3-dimensional qubit Quart: 4-dimensional qubit Qudit: d-dimensional qubit
All quantum objects store their data in tensor format, preserving the structure of individual qudits. This enables efficient tensor network operations and makes qudit indexing natural. The .matrix property provides the flattened matrix representation when needed.
-
State Vectors: Tensor with shape
(*ensemble, d0, d1, ...)where eachdiis a qudit dimension- Example: 2-qubit state has shape
(2, 2) .matrixreturns shape(*ensemble, prod(dims))
- Example: 2-qubit state has shape
-
Density Matrices: Tensor with shape
(*ensemble, d0_out, d1_out, ..., d0_in, d1_in, ...)- Example: 2-qubit density matrix has shape
(2, 2, 2, 2) .matrixreturns shape(*ensemble, prod(dims), prod(dims))
- Example: 2-qubit density matrix has shape
-
Unitaries/Operators: Tensor with shape
(*ensemble, d0_out, d1_out, ..., d0_in, d1_in, ...)- Example: 2-qubit unitary has shape
(2, 2, 2, 2) .matrixreturns shape(*ensemble, prod(dims_out), prod(dims_in))
- Example: 2-qubit unitary has shape
-
Kraus Maps: Tensor with shape
(*ensemble, num_kraus, d0_out, d1_out, ..., d0_in, d1_in, ...)- Example: single-qubit Kraus map with 4 operators has shape
(4, 2, 2) .matrixreturns shape(*ensemble, num_kraus, d_out, d_in)
- Example: single-qubit Kraus map with 4 operators has shape
-
Superoperators (SuperOp, Choi, PauliLiouville): Tensor with 4 groups of dimensions:
(*ensemble, d0_out_bra, ..., d0_out_ket, ..., d0_in_bra, ..., d0_in_ket, ...)- Example: single-qubit superoperator has shape
(2, 2, 2, 2) .matrixreturns shape(*ensemble, prod(dims_out)**2, prod(dims_in)**2)
- Example: single-qubit superoperator has shape
-
QuantumInstruments: Tensor with an outcome axis followed by 4 groups of dimensions:
(*ensemble, num_outcomes, d0_out_bra, ..., d0_out_ket, ..., d0_in_bra, ..., d0_in_ket, ...)- Each outcome slice is a CP (but not TP) superoperator; the sum over outcomes is CPTP.
- Example: single-qubit ideal measurement has shape
(2, 2, 2, 2, 2) .matrixreturns shape(*ensemble, num_outcomes, prod(dims_out)**2, prod(dims_in)**2)- Key properties:
confusion_matrix,transition_matrix,measured_qudits - Supports
@(compose) and|(tensor product) operators.
- Add implementation to
src/quax/gates.py - Gate will be automatically available via
qx.gates.GATE_NAME - Add tests to
tests/test_quantum_objects.py - Document in appropriate
.rstfile
Common noise channels are defined canonically as Lindbladian generators in
src/quax/lindbladians.py (rate-parameterized; obtain the CPTP channel via qx.evolve(L, t)).
Prefer adding a new noise channel there.
- For a noise generator: add a factory to
src/quax/lindbladians.pyreturning aLindbladian. - For a SuperOp channel or a fixed non-divisible channel / instrument (no Lindbladian generator):
implement in
src/quax/channels.py. - Export from
__init__.py. - Add tests to
tests/test_lindbladian.py,tests/test_channels.py, ortests/test_common_channels.pyas appropriate.
- Implement conversion in
src/quax/_superoperator_transformations.py - Follow naming pattern:
{source}_to_{target}(e.g.,choi_to_kraus) - Add tests to
tests/test_superoperator_transformations.py - Ensure inverse transformation exists when applicable
- Implement in
src/quax/_metrics.py - Export from
__init__.py - Add tests to
tests/test_metrics.py - Include validation for input types
Citing papers in the docstrings or comments is a good practice. We use the following format. The title is abbreviated by a handful of letters and am arxiv URL is included wherever possible.
.. [GRAPTN] Tensor networks and graphical calculus for open quantum systems.
Wood et al.
Quant. Inf. Comp. 15, 0579-0811 (2015).
(no DOI)
https://arxiv.org/abs/1111.6950
There is currently no centralized bibliography, but that may change in the future.
- Use pytest fixtures defined in
conftest.py - Test files mirror source file structure (
test_apply.py↔_apply.py) - Use
reference_pauli_liouville.pyfor reference implementations
- Unit Tests: Test individual functions in isolation
- Integration Tests: Test composed operations
- Validation Tests: Verify mathematical properties (unitarity, CPTP, etc.)
- Comparison Tests: Compare against reference implementations (QuTiP)
import jax
import jax.numpy as jnp
import quax as qx
def test_example():
# Setup
key = jax.random.PRNGKey(42)
state = qx.zero_state_vector(dims=(2,))
# Action
U = qx.random_unitary(dims=(2,), key=key)
result = qx.apply_unitary_to_state_vector(U, state)
# Assert
assert jnp.allclose(jnp.linalg.norm(result), 1.0)
# Using gates from the gates submodule
state_after_hadamard = qx.apply_unitary_to_state_vector(qx.gates.H, state)- JAX Arrays are Immutable: Don't use in-place operations
- Random Number Generation: Always pass
keyparameter explicitly - Shape Dimensions: Use
dimstuples, not direct array shapes - Type Annotations: Use
jax.Arraynotnp.ndarray - Imports: Don't import from private modules (
_*.py) outside the package
- JAX: Automatic differentiation and GPU acceleration
- QuTiP: Used for testing/validation (dev dependency only)
- Nielsen, M. A., & Chuang, I. L. (2001). Quantum computation and quantum information (Vol. 2). Cambridge: Cambridge university press.
- Hashim, A., Nguyen, L. B., Goss, N., Marinelli, B., Naik, R. K., Chistolini, T., ... & Siddiqi, I. (2024). A practical introduction to benchmarking and characterization of quantum computers. arXiv preprint arXiv:2408.12064.
- Wood, C. J., Biamonte, J. D., & Cory, D. G. (2011). Tensor networks and graphical calculus for open quantum systems. arXiv preprint arXiv:1111.6950.
- Always run tests after making changes
- Check type hints with pyright
- Verify JAX compatibility (no NumPy-specific operations)
- Update docstrings for public API changes
- Check exports in
__init__.pyfor new public functions - Use
complexrather thanjnp.complex128orjnp.complex64.
- Follow existing patterns in similar functions
- Add comprehensive tests including edge cases
- Document mathematical background in docstrings
- Consider GPU compatibility (avoid Python loops)
- Validate quantum properties (unitarity, CPTP, trace preservation)
- Check array shapes with
jnp.shape - Verify numerical precision with
jnp.allclose - Test with simple cases (single qubit, known gates)
- Compare with QuTiP when available
- Use
jax.debug.printfor debugging JIT-compiled functions
Copyright 2026 Rigetti & Co, LLC. Licensed under Apache License 2.0.
For questions or issues, refer to the project maintainers at Rigetti Computing.