diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fcaea98..598369fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 with a stabilizer-rank strategy. Without a mask, exactly `shots` rows are returned. `sample` warns when a masked detector is a constant 1, which discards every shot. (#252) +- The public API is `tsim.Circuit`, `tsim.CompiledMeasurementSampler` and + `tsim.CompiledDetectorSampler`, which follow semantic versioning. Every other + module is internal and may change without notice, and the API reference + documents only the public and experimental objects. `CompiledStateProbs`, + `SteaneEncoder` and `ColorEncoder5` are importable from `tsim.experimental`, + which may change in any release. The stabilizer rank strategies (`"cat5"`, + `"bss"`, `"cutting"`) are provisional: they may be removed in a minor + release, after at least one minor release in which they emit a + `DeprecationWarning`. A seed reproduces its samples only with the same + versions of Tsim and its dependencies and the same backend (CPU or GPU), and, + with a stabilizer-rank strategy, the same explicit `batch_size`. ### Added - `CompiledMeasurementSampler.sample(bit_packed=True)`, as in Stim. diff --git a/docs/demos/magic_state_distillation.ipynb b/docs/demos/magic_state_distillation.ipynb index fe956691..8acf77bb 100644 --- a/docs/demos/magic_state_distillation.ipynb +++ b/docs/demos/magic_state_distillation.ipynb @@ -19,7 +19,7 @@ "import numpy as np\n", "from typing import Literal\n", "import matplotlib.pyplot as plt\n", - "from tsim.utils.encoder import ColorEncoder5" + "from tsim.experimental import ColorEncoder5" ] }, { @@ -338,7 +338,7 @@ "# Encoding the distillation circuit\n", "\n", "To address this issue, we need to make our distillation circuit resilient to noise.\n", - "We do this by encoding it using a quantum error correction code, specifically the [17,1,5] color code. The helper class `ColorEncoder5` provides a convenient way to encode the distillation circuit. Each qubit will be encoded into 17 physical qubits using an encoding circuit. After that, each gate of the distillation circuit will be applied transversally to the encoded qubits." + "We do this by encoding it using a quantum error correction code, specifically the [17,1,5] color code. The experimental helper class `tsim.experimental.ColorEncoder5` provides a convenient way to encode the distillation circuit. Each qubit will be encoded into 17 physical qubits using an encoding circuit. After that, each gate of the distillation circuit will be applied transversally to the encoded qubits." ] }, { diff --git a/docs/demos/overview.ipynb b/docs/demos/overview.ipynb index c33591a4..871f7c0b 100644 --- a/docs/demos/overview.ipynb +++ b/docs/demos/overview.ipynb @@ -634,7 +634,7 @@ "id": "54", "metadata": {}, "source": [ - "Finally, Tsim allows to compute probability values for target states via the `CompiledStateProbs` sampler." + "Finally, Tsim allows to compute probability values for target states via the experimental `tsim.experimental.CompiledStateProbs` sampler, which may change in any release." ] }, { @@ -645,7 +645,7 @@ "outputs": [], "source": [ "import numpy as np\n", - "from tsim.sampler import CompiledStateProbs\n", + "from tsim.experimental import CompiledStateProbs\n", "\n", "sampler = CompiledStateProbs(c)" ] diff --git a/docs/index.md b/docs/index.md index 698c42a9..67cabeb3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -60,6 +60,8 @@ Choose a strategy with the `strategy` argument of `compile_sampler` and `compile sampler = c.compile_detector_sampler(strategy="cat5") ``` +The stabilizer rank strategies are provisional; see [API stability](#api-stability). + The [Tsim 1.0 blog post](https://bloqade.quera.com/blog/introducing-tsim-1-0-0/) introduces the active state vector strategy and compares its performance with other simulators. ## Architecture @@ -170,6 +172,12 @@ the noiseless reference sample, and otherwise when their raw parities are 0. A masked detector that is a constant 1, such as one of a noiseless circuit whose measurements have parity 1, discards every shot; `sample` warns about it. +## API stability + +Tsim follows [semantic versioning](https://semver.org/) for its public API: `tsim.Circuit`, `tsim.CompiledMeasurementSampler` and `tsim.CompiledDetectorSampler`. `tsim.experimental` holds helpers that may change in any release. Every other module is internal, even where it is importable. The stabilizer rank [sampling strategies](#sampling-strategies) (`"cat5"`, `"bss"`, `"cutting"`) are provisional: they may be removed in a minor release, after at least one minor release in which they emit a `DeprecationWarning`. See the [API reference](reference/index.md). + +A seed reproduces its samples only with the same versions of Tsim and its dependencies (such as NumPy and JAX) and the same backend (CPU or GPU). With a stabilizer-rank strategy, pass the same explicit `batch_size` as well, since the default batch size depends on the free memory. + ## Citing Tsim If you use Tsim, please consider citing the paper describing the core simulation approach: ``` diff --git a/docs/scripts/gen_ref_nav.py b/docs/scripts/gen_ref_nav.py index 58a711e3..7af89180 100644 --- a/docs/scripts/gen_ref_nav.py +++ b/docs/scripts/gen_ref_nav.py @@ -1,55 +1,45 @@ # type: ignore -"""Generate the code reference pages and navigation.""" - -from pathlib import Path +"""Generate the API reference pages for Tsim's public and experimental API.""" import mkdocs_gen_files -SRC_PATH = "src" - -skip_keywords = [] +PAGES = { + "index.md": ( + "Tsim API", + "The public API, which follows semantic versioning. Other modules are " + "internal and may change without notice.", + [ + "tsim.Circuit", + "tsim.CompiledMeasurementSampler", + "tsim.CompiledDetectorSampler", + ], + ), + "experimental.md": ( + "Experimental", + "Helpers outside of the stable API. They may change or be removed in any " + "release.", + [ + "tsim.experimental.CompiledStateProbs", + "tsim.experimental.SteaneEncoder", + "tsim.experimental.ColorEncoder5", + ], + ), +} + +# NOTE: the encoders' methods are inherited from an internal base class, and the +# bases would name internal classes. +OPTIONS = """ options: + inherited_members: true + show_bases: false + show_inheritance_diagram: false +""" nav = mkdocs_gen_files.Nav() -for path in sorted(Path(SRC_PATH).rglob("*.py")): - module_path = path.relative_to(SRC_PATH).with_suffix("") - doc_path = path.relative_to(SRC_PATH).with_suffix(".md") - full_doc_path = Path("reference", doc_path) - - iskip = False - - # Skip anything in the external folder - if "external" in str(doc_path): - iskip = True - - for kwrd in skip_keywords: - if kwrd in str(doc_path): - iskip = True - break - if iskip: - print("[Ignore]", str(doc_path)) - continue - - print("[>]", str(doc_path)) - - parts = tuple(module_path.parts) - - if parts[-1] == "__init__": - parts = parts[:-1] - doc_path = doc_path.with_name("index.md") - full_doc_path = full_doc_path.with_name("index.md") - - # [note] if selectionally ignore some files - # elif parts[-1].startswith("_wrapper"): - # pass - # else: - # continue - - nav[parts] = doc_path.as_posix() - with mkdocs_gen_files.open(full_doc_path, "w") as fd: - ident = ".".join(parts) - fd.write(f"::: {ident}") - - mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path) +for doc_path, (title, intro, objects) in PAGES.items(): + nav[title] = doc_path + with mkdocs_gen_files.open(f"reference/{doc_path}", "w") as fd: + fd.write(f"# {title}\n\n{intro}\n\n") + fd.writelines(f"::: {obj}\n{OPTIONS}\n" for obj in objects) with mkdocs_gen_files.open("reference/SUMMARY.txt", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) diff --git a/src/tsim/__init__.py b/src/tsim/__init__.py index 531a858f..9453b7d1 100644 --- a/src/tsim/__init__.py +++ b/src/tsim/__init__.py @@ -4,15 +4,9 @@ It exposes `Circuit`, `CompiledDetectorSampler`, and `CompiledMeasurementSampler` objects, which follow the Stim API. -The package is organized as follows: - -- `circuit.py`: The thin `Circuit` wrapper around `stim.Circuit`. -- `sampler.py`: Orchestrates compilation and evaluation to sample measurements or detectors. -- `core/`: Builds quantum/classical ZX graphs with zxparam; parsing and data types. -- `compile/`: Compiles zxparam's stabilizer-rank decompositions to JAX. -- `statevector/`: Samples non-direct components as Pauli rotations and measurements on a dense state vector (`strategy="active_statevector"`). -- `noise/`: Pauli noise channels and detector error models. -- `utils/`: Utility functions for visualization and linear algebra. +These three objects are Tsim's public API, which follows semantic versioning. +`tsim.experimental` holds helpers that may change in any release. Every other +module is internal and may change without notice, even where it is importable. """ __version__ = "0.1.5" @@ -22,3 +16,5 @@ CompiledDetectorSampler as CompiledDetectorSampler, CompiledMeasurementSampler as CompiledMeasurementSampler, ) + +__all__ = ["Circuit", "CompiledDetectorSampler", "CompiledMeasurementSampler"] diff --git a/src/tsim/circuit.py b/src/tsim/circuit.py index ddb0fe53..f4e25210 100644 --- a/src/tsim/circuit.py +++ b/src/tsim/circuit.py @@ -953,12 +953,16 @@ def compile_sampler( non-direct component on a dense state vector over its active qubits, or a stabilizer rank decomposition strategy ("cat5", "bss", "cutting"). "active_statevector" samples on the CPU, on - ``numba.get_num_threads()`` threads. - seed: Random seed for the sampler. If None, a random seed is drawn. With - "active_statevector", and under any strategy for a circuit without + ``numba.get_num_threads()`` threads. The stabilizer rank + strategies are provisional. + seed: Random seed for the sampler. If None, a random seed is drawn. A seed + reproduces its samples only with the same versions of Tsim and its + dependencies (such as NumPy and JAX) and the same backend (CPU or GPU). + With "active_statevector", and under any strategy for a circuit without non-direct components, the samples of a seed depend on neither the batch - size nor the thread count; with a stabilizer-rank strategy they are only - deterministic for a fixed batch size. + size nor the thread count. With a stabilizer-rank strategy they also + need the same explicit ``batch_size``, since the default batch size + depends on the free memory. Returns: A CompiledMeasurementSampler that can be used to sample measurements. @@ -986,13 +990,16 @@ def compile_detector_sampler( non-direct component on a dense state vector over its active qubits, or a stabilizer rank decomposition strategy ("cat5", "bss", "cutting"). "active_statevector" samples on the CPU, on - ``numba.get_num_threads()`` threads. - seed: Random seed for the sampler. If None, a random seed is drawn. With - "active_statevector", and under any strategy for a circuit without + ``numba.get_num_threads()`` threads. The stabilizer rank + strategies are provisional. + seed: Random seed for the sampler. If None, a random seed is drawn. A seed + reproduces its samples only with the same versions of Tsim and its + dependencies (such as NumPy and JAX) and the same backend (CPU or GPU). + With "active_statevector", and under any strategy for a circuit without non-direct components, the samples of a seed depend on neither the batch - size nor the thread count; with a stabilizer-rank strategy they are only - deterministic for a fixed batch size and fixed reference sample - settings. + size nor the thread count. With a stabilizer-rank strategy they also + need the same explicit ``batch_size`` and the same reference sample + settings, since the default batch size depends on the free memory. Returns: A CompiledDetectorSampler that can be used to sample detectors and observables. diff --git a/src/tsim/experimental.py b/src/tsim/experimental.py new file mode 100644 index 00000000..e1941e3e --- /dev/null +++ b/src/tsim/experimental.py @@ -0,0 +1,18 @@ +"""Experimental helpers, outside of Tsim's stable API. + +The objects here may change or be removed in any release, including patch +releases. + +- `CompiledStateProbs` computes the probabilities of given measurement or + detector outcomes with a stabilizer rank decomposition. +- `SteaneEncoder` and `ColorEncoder5` encode a circuit transversally into the + [[7,1,3]] Steane code and the [[17,1,5]] color code. +""" + +from tsim.sampler import CompiledStateProbs as CompiledStateProbs +from tsim.utils.encoder import ( + ColorEncoder5 as ColorEncoder5, + SteaneEncoder as SteaneEncoder, +) + +__all__ = ["ColorEncoder5", "CompiledStateProbs", "SteaneEncoder"] diff --git a/src/tsim/sampler.py b/src/tsim/sampler.py index 55d9b0ed..c9183d72 100644 --- a/src/tsim/sampler.py +++ b/src/tsim/sampler.py @@ -313,12 +313,16 @@ def __init__( non-direct component on a dense state vector over its active qubits, or a stabilizer rank decomposition strategy ("cat5", "bss", "cutting"). "active_statevector" samples on the CPU, on - ``numba.get_num_threads()`` threads. - seed: Random seed for the sampler. If None, a random seed is drawn. With - "active_statevector", and under any strategy for a circuit without + ``numba.get_num_threads()`` threads. The stabilizer rank + strategies are provisional. + seed: Random seed for the sampler. If None, a random seed is drawn. A seed + reproduces its samples only with the same versions of Tsim and its + dependencies (such as NumPy and JAX) and the same backend (CPU or GPU). + With "active_statevector", and under any strategy for a circuit without non-direct components, the samples of a seed depend on neither the batch - size nor the thread count; with a stabilizer-rank strategy they are only - deterministic for a fixed batch size. + size nor the thread count. With a stabilizer-rank strategy they also + need the same explicit ``batch_size``, since the default batch size + depends on the free memory. """ if seed is None: @@ -767,12 +771,16 @@ def __init__( Args: circuit: The quantum circuit to compile. strategy: "active_statevector" (the default), or a stabilizer rank - decomposition strategy ("cat5", "bss", "cutting"). - seed: Random seed for the sampler. If None, a random seed is drawn. With - "active_statevector", and under any strategy for a circuit without + decomposition strategy ("cat5", "bss", "cutting"). The + stabilizer rank strategies are provisional. + seed: Random seed for the sampler. If None, a random seed is drawn. A seed + reproduces its samples only with the same versions of Tsim and its + dependencies (such as NumPy and JAX) and the same backend (CPU or GPU). + With "active_statevector", and under any strategy for a circuit without non-direct components, the samples of a seed depend on neither the batch - size nor the thread count; with a stabilizer-rank strategy they are only - deterministic for a fixed batch size. + size nor the thread count. With a stabilizer-rank strategy they also + need the same explicit ``batch_size``, since the default batch size + depends on the free memory. """ super().__init__( @@ -863,13 +871,16 @@ def __init__( Args: circuit: The quantum circuit to compile. strategy: "active_statevector" (the default), or a stabilizer rank - decomposition strategy ("cat5", "bss", "cutting"). - seed: Random seed for the sampler. If None, a random seed is drawn. With - "active_statevector", and under any strategy for a circuit without + decomposition strategy ("cat5", "bss", "cutting"). The + stabilizer rank strategies are provisional. + seed: Random seed for the sampler. If None, a random seed is drawn. A seed + reproduces its samples only with the same versions of Tsim and its + dependencies (such as NumPy and JAX) and the same backend (CPU or GPU). + With "active_statevector", and under any strategy for a circuit without non-direct components, the samples of a seed depend on neither the batch - size nor the thread count; with a stabilizer-rank strategy they are only - deterministic for a fixed batch size and fixed reference sample - settings. + size nor the thread count. With a stabilizer-rank strategy they also + need the same explicit ``batch_size`` and the same reference sample + settings, since the default batch size depends on the free memory. """ super().__init__( @@ -1062,8 +1073,10 @@ def __init__( sample_detectors: If True, compute detector/observable probabilities. strategy: Stabilizer rank decomposition strategy ("cat5", "bss", "cutting"); "active_statevector" cannot compute probabilities. - seed: Random seed. If None, a random seed is generated. Note that - deterministic results are only guaranteed for a fixed batch size. + seed: Random seed. If None, a random seed is generated. Results are + only deterministic for a fixed batch size, the same versions of + Tsim and its dependencies (such as NumPy and JAX) and the same + backend (CPU or GPU). """ super().__init__( diff --git a/test/unit/test_public_api.py b/test/unit/test_public_api.py new file mode 100644 index 00000000..8c4eb956 --- /dev/null +++ b/test/unit/test_public_api.py @@ -0,0 +1,24 @@ +import tsim +import tsim.experimental +from tsim.sampler import CompiledStateProbs +from tsim.utils.encoder import ColorEncoder5, SteaneEncoder + + +def test_public_api(): + assert sorted(tsim.__all__) == [ + "Circuit", + "CompiledDetectorSampler", + "CompiledMeasurementSampler", + ] + assert all(hasattr(tsim, name) for name in tsim.__all__) + + +def test_experimental_api(): + assert tsim.experimental.CompiledStateProbs is CompiledStateProbs + assert tsim.experimental.ColorEncoder5 is ColorEncoder5 + assert tsim.experimental.SteaneEncoder is SteaneEncoder + assert sorted(tsim.experimental.__all__) == [ + "ColorEncoder5", + "CompiledStateProbs", + "SteaneEncoder", + ]