diff --git a/.ado/publish.yml b/.ado/publish.yml index 46af8b11d4..a5f79ba268 100644 --- a/.ado/publish.yml +++ b/.ado/publish.yml @@ -289,11 +289,10 @@ extends: python ./prereqs.py --skip-wasm && python ./version.py displayName: Install Prereqs and set version - # The jupyterlab and widgets packages have no tests. The qdk package has minimal tests but - # they also depend on the qsharp package which is platform-dependent. So we skip tests here - # and rely on the GitHub CI to run the few `qdk` tests on every PR. + # Build platform-agnostic Python wheels: jupyterlab, widgets, and the + # qsharp shim (pure Python). The qdk native wheel is built per-platform. - script: | - python ./build.py --jupyterlab --widgets --qdk --no-check --no-test --no-check-prereqs + python ./build.py --jupyterlab --widgets --pip --no-check --no-test --no-check-prereqs displayName: Build Platform-Agnostic Packages - script: | @@ -398,13 +397,13 @@ extends: # Windows arm64 - script: | - python build.py --pip --no-check-prereqs --no-integration-tests --no-optional-dependencies + python build.py --qdk --no-check-prereqs --no-integration-tests --no-optional-dependencies displayName: Build Platform-Dependent Py Packages condition: and(eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['arch'], 'aarch64')) # every other platform - script: | - python build.py --pip --no-check-prereqs --integration-tests + python build.py --qdk --no-check-prereqs --integration-tests displayName: Build Platform-Dependent Py Packages condition: not(and(eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['arch'], 'aarch64'))) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5edd1862eb..c6e627a2de 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -19,7 +19,8 @@ /library @swernli @orpuente-MS /source/npm @billti @minestarks @ScottCarda-MS /source/pip @billti @idavis @minestarks -/source/pip/qsharp/qre @msoeken @brad-lackey @jwhogabo +/source/qdk_package @billti @idavis @minestarks +/source/qdk_package/qdk/qre @msoeken @brad-lackey @jwhogabo /source/playground @billti @minestarks /source/qre @msoeken @brad-lackey @jwhogabo /source/resource_estimator @billti @swernli diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7d0098bc05..ae42a474bc 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,7 +10,7 @@ All internal source code for the compiler and related tooling has been moved und Most of the core components are implemented in Rust. These components are packaged in two ways: -1. Compiled as a native Python module and packaged into the `qsharp` Python package +1. Compiled as a native Python module and packaged into the `qdk` Python package 2. Compiled into WebAssembly and packaged into the `qsharp-lang` npm package ## Repo Contents @@ -52,7 +52,8 @@ Most of the core components are implemented in Rust. These components are packag **Python** -- **pip/**: The `qsharp` Python package +- **qdk_package/**: The `qdk` Python package (core package with native Rust extension) +- **pip/**: The `qsharp` Python package (thin deprecation shim that re-exports from `qdk`) - **jupyterlab/**: JupyterLab extension for Q# - **widgets/**: Q# Jupyter widgets @@ -82,7 +83,8 @@ Most of the core components are implemented in Rust. These components are packag - `./build.py` runs full CI checks, including lints and unit tests. - `./build.py --wasm --npm --vscode` only builds the VS Code extension, including its dependencies the WASM module and the `qsharp-lang` npm package. -- `./build.py --pip` only builds the `qsharp` Python package, including its native dependencies. +- `./build.py --qdk` only builds the `qdk` Python package, including its native dependencies. +- `./build.py --pip` only builds the `qsharp` shim package (requires `qdk` to be built first). - Pass `--no-check` to `./build.py`, in combination with any other command line options, to skip the lints and formatting checks. - When working in Rust parts of the codebase, using `cargo` commands is usually more efficient than building via `./build.py`. - Many lints can be auto-fixed via `cargo clippy --fix`. diff --git a/.gitignore b/.gitignore index 6daec1d027..158360abbc 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ __pycache__/ /source/fuzz/coverage /source/fuzz/Cargo.lock /source/pip/doc +/source/pip/build/ +/source/pip/*.egg-info/ /source/samples_test/src/tests/*_generated.rs .mypy_cache/ .pytest_cache/ diff --git a/.prettierignore b/.prettierignore index 6ba56467cb..f2253032c5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,6 +20,8 @@ __pycache__/ /source/npm/qsharp/test/**/*.snapshot.* /source/pip/ /source/pip/src/**/*.html +/source/qdk_package/ +/source/qdk_package/src/**/*.html /source/playground/public/libs/ /source/vscode/out/ /source/vscode/test/out/ diff --git a/Cargo.lock b/Cargo.lock index 432d8283f7..d72676991b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2081,6 +2081,30 @@ dependencies = [ "cc", ] +[[package]] +name = "qdk" +version = "0.0.0" +dependencies = [ + "allocator", + "expect-test", + "memchr", + "miette", + "noisy_simulator", + "num-bigint", + "num-complex", + "num-traits", + "pyo3", + "qdk_simulators", + "qre", + "qsc", + "rand 0.8.5", + "rayon", + "resource_estimator", + "rustc-hash", + "serde", + "serde_json", +] + [[package]] name = "qdk_simulators" version = "0.0.0" @@ -2515,30 +2539,6 @@ dependencies = [ "wasm-bindgen-futures", ] -[[package]] -name = "qsharp" -version = "0.0.0" -dependencies = [ - "allocator", - "expect-test", - "memchr", - "miette", - "noisy_simulator", - "num-bigint", - "num-complex", - "num-traits", - "pyo3", - "qdk_simulators", - "qre", - "qsc", - "rand 0.8.5", - "rayon", - "resource_estimator", - "rustc-hash", - "serde", - "serde_json", -] - [[package]] name = "qsls" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 688b27457b..04de7dfc9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ members = [ "source/index_map", "source/language_service", "source/simulators", - "source/pip", + "source/qdk_package", "source/qre", "source/resource_estimator", "source/samples_test", diff --git a/build.py b/build.py index b428489a3c..7ecca035e5 100755 --- a/build.py +++ b/build.py @@ -300,20 +300,6 @@ def use_python_env(folder): step_end() -def install_qsharp_python_package(cwd, wheelhouse, interpreter): - command_args = [ - interpreter, - "-m", - "pip", - "install", - "--force-reinstall", - "--no-index", - "--find-links=" + wheelhouse, - "qsharp", - ] - subprocess.run(command_args, check=True, text=True, cwd=cwd) - - # If any package fails to install when using a requirements file, the entire # process will fail with unpredicatable state of installed packages. To avoid # this, we install each package individually from the requirements file. @@ -346,7 +332,7 @@ def install_python_test_requirements(cwd, interpreter, check: bool = True): subprocess.run(command_args, check=check, text=True, cwd=cwd) -def build_qsharp_wheel(cwd, interpreter, pip_env): +def build_maturin_wheel(cwd, interpreter, pip_env): # Read the build dependencies out of the pyproject.toml and install them first. with open(os.path.join(cwd, "pyproject.toml"), "rb") as f: requires = tomllib.load(f)["build-system"]["requires"] @@ -438,32 +424,64 @@ def run_ci_historic_benchmark(): f.write(result.stdout) -if build_pip: - step_start("Building the pip package") +if build_qdk: + step_start("Building the qdk python package") - (python_bin, pip_env) = use_python_env(pip_src) + # Reuse (or create) the pip environment so qdk wheel can be built/installed consistently. + python_bin, pip_env = use_python_env(qdk_python_src) - build_qsharp_wheel(pip_src, python_bin, pip_env) + # Build the qdk wheel with maturin (it now owns the native extension). + build_maturin_wheel(qdk_python_src, python_bin, pip_env) step_end() if run_tests: - step_start("Running tests for the pip package") + step_start("Running tests for the qdk python package") + # Install per-package test requirements (pytest, etc.) + install_python_test_requirements(qdk_python_src, python_bin) - install_python_test_requirements(pip_src, python_bin) - install_qsharp_python_package(pip_src, wheels_dir, python_bin) - run_python_tests(os.path.join(pip_src, "tests"), python_bin, pip_env) + # Install qdk from the freshly built wheel. + # Use --no-deps because dependencies (pyqir, etc.) are already installed + # via test_requirements, and --no-index can't resolve them from PyPI. + install_args = [ + python_bin, + "-m", + "pip", + "install", + "--force-reinstall", + "--no-deps", + "--no-index", + "--find-links=" + wheels_dir, + "qdk", + ] + subprocess.run(install_args, check=True, text=True, cwd=qdk_python_src) + # Run its test suite + run_python_tests(os.path.join(qdk_python_src, "tests"), python_bin, pip_env) step_end() if args.integration_tests: - step_start("Setting up for integration tests for the pip package") - test_dir = os.path.join(pip_src, "tests-integration") + step_start("Setting up for integration tests for the qdk package") + test_dir = os.path.join(qdk_python_src, "tests-integration") install_python_test_requirements(test_dir, python_bin, check=False) + + # Install qdk from the freshly built wheel. + install_args = [ + python_bin, + "-m", + "pip", + "install", + "--force-reinstall", + "--no-deps", + "--no-index", + "--find-links=" + wheels_dir, + "qdk", + ] + subprocess.run(install_args, check=True, text=True, cwd=test_dir) step_end() for version in QISKIT_VERSION_MATRIX: step_start( - f"Running integration tests for the pip package ({version['label']})" + f"Running integration tests for the qdk package ({version['label']})" ) version_install_args = [ @@ -477,21 +495,18 @@ def run_ci_historic_benchmark(): ] + version["requirements"] subprocess.run(version_install_args, check=True, text=True, cwd=test_dir) - install_qsharp_python_package(pip_src, wheels_dir, python_bin) - run_python_integration_tests(test_dir, python_bin) step_end() +if build_pip: + step_start("Building the pip package") -if build_qdk: - step_start("Building the qdk python package") - - # Reuse (or create) the pip environment so qsharp wheel can be built/installed consistently. - (python_bin, pip_env) = use_python_env(qdk_python_src) + python_bin, pip_env = use_python_env(pip_src) - # Build the qdk wheel (no dependency build needed; it's a thin meta-package) - qdk_build_args = [ + # qsharp is now a pure-Python shim depending on qdk. + # Build with setuptools (no maturin needed). + pip_build_args = [ python_bin, "-m", "build", @@ -499,42 +514,16 @@ def run_ci_historic_benchmark(): "-v", "--outdir", wheels_dir, - qdk_python_src, + pip_src, ] - subprocess.run(qdk_build_args, check=True, text=True, cwd=qdk_python_src) + subprocess.run(pip_build_args, check=True, text=True, cwd=pip_src, env=pip_env) step_end() - if run_tests: - step_start("Running tests for the qdk python package") - # Install per-package test requirements (pytest, etc.) - install_python_test_requirements(qdk_python_src, python_bin) - - # Install qsharp wheel first so dependency resolution is offline & version-synced. - install_qsharp_python_package(qdk_python_src, wheels_dir, python_bin) - - # Install qdk itself from the freshly built wheel (force to ensure isolation) - install_args = [ - python_bin, - "-m", - "pip", - "install", - "--force-reinstall", - "--no-index", - "--no-deps", - "--find-links=" + wheels_dir, - "qdk", - "qsharp", - ] - subprocess.run(install_args, check=True, text=True, cwd=qdk_python_src) - - # Run its test suite - run_python_tests(os.path.join(qdk_python_src, "tests"), python_bin, pip_env) - step_end() if build_widgets: step_start("Building the Python widgets") - (python_bin, _) = use_python_env(qdk_python_src) + python_bin, _ = use_python_env(qdk_python_src) widgets_build_args = [ python_bin, @@ -660,7 +649,7 @@ def run_ci_historic_benchmark(): if build_jupyterlab: step_start("Building the JupyterLab extension") - (python_bin, _) = use_python_env(jupyterlab_src) + python_bin, _ = use_python_env(jupyterlab_src) pip_build_args = [ python_bin, @@ -697,7 +686,7 @@ def run_ci_historic_benchmark(): or f.startswith("benzene.") ) ] - (python_bin, pip_env) = use_python_env(samples_src) + python_bin, pip_env = use_python_env(samples_src) # Install the qsharp package pip_install_args = [ @@ -814,7 +803,7 @@ def _run_notebooks(files): dir for dir, _, _ in project_directories if dir.find("testing") != -1 ] - install_python_test_requirements(pip_src, python_bin) + install_python_test_requirements(os.path.join(samples_src, "testing"), python_bin) for test_project_dir in test_projects_directories: run_python_tests(test_project_dir, python_bin, pip_env) step_end() diff --git a/samples/qre/1_qre_input.ipynb b/samples/qre/1_qre_input.ipynb index a6f07d2d9d..8a39346a81 100644 --- a/samples/qre/1_qre_input.ipynb +++ b/samples/qre/1_qre_input.ipynb @@ -124,7 +124,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "24dad0bb", "metadata": {}, "outputs": [], @@ -132,7 +132,7 @@ "# Load a pre-compiled QIR file for the Hidden Shift algorithm\n", "qir_file = (\n", " Path.cwd().parent.parent\n", - " / \"source\" / \"pip\" / \"tests-integration\" / \"resources\"\n", + " / \"source\" / \"qdk_package\" / \"tests-integration\" / \"resources\"\n", " / \"adaptive_ri\" / \"output\" / \"HiddenShiftNISQ.ll\"\n", ")\n", "qir_app = QIRApplication(qir_file.read_text(encoding=\"utf-8\"))" diff --git a/samples/qre/2_analysing_results.ipynb b/samples/qre/2_analysing_results.ipynb index ca9cdf72be..da2c2a2d47 100644 --- a/samples/qre/2_analysing_results.ipynb +++ b/samples/qre/2_analysing_results.ipynb @@ -192,7 +192,7 @@ } ], "source": [ - "from qsharp.code import EstimateAdder\n", + "from qdk.code import EstimateAdder\n", "\n", "app = QSharpApplication(EstimateAdder)\n", "arch = GateBased(error_rate=1e-4, gate_time=100, measurement_time=500)\n", diff --git a/source/pip/test_requirements.txt b/samples/testing/test_requirements.txt similarity index 100% rename from source/pip/test_requirements.txt rename to samples/testing/test_requirements.txt diff --git a/source/pip/README.md b/source/pip/README.md index dfc229221d..98e0c999a7 100644 --- a/source/pip/README.md +++ b/source/pip/README.md @@ -1,20 +1,20 @@ # Q# Language Support for Python +> **Note:** The `qsharp` package is deprecated. Please use the [`qdk`](https://pypi.org/project/qdk/) package instead. This package is a thin compatibility shim that re-exports the `qdk` public API so that existing code continues to work. + Q# is an open-source, high-level programming language for developing and running quantum algorithms. The `qsharp` package for Python provides interoperability with the Q# interpreter, making it easy to simulate Q# programs within Python. ## Installation -To install the Q# language package, run: - ```bash -pip install qsharp +pip install qdk ``` -## Usage +For backward compatibility, `pip install qsharp` also works and will install `qdk` as a dependency. -First, import the `qsharp` module: +## Usage ```python from qdk import qsharp @@ -42,7 +42,7 @@ BellState() ## Telemetry This library sends telemetry. Minimal anonymous data is collected to help measure feature usage and performance. -All telemetry events can be seen in the source file [telemetry_events.py](https://github.com/microsoft/qdk/tree/main/source/pip/qsharp/telemetry_events.py). +All telemetry events can be seen in the source file [telemetry_events.py](https://github.com/microsoft/qdk/tree/main/source/qdk_package/qdk/telemetry_events.py). To disable sending telemetry from this package, set the environment variable `QDK_PYTHON_TELEMETRY=none` diff --git a/source/pip/pyproject.toml b/source/pip/pyproject.toml index 1e16486b09..ba1b419af5 100644 --- a/source/pip/pyproject.toml +++ b/source/pip/pyproject.toml @@ -3,8 +3,9 @@ name = "qsharp" version = "0.0.0" readme = "README.md" requires-python = ">= 3.10" +dependencies = ["qdk==0.0.0"] +license = "MIT" classifiers = [ - "License :: OSI Approved :: MIT License", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", @@ -13,22 +14,25 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python", - "Programming Language :: Rust", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", ] [project.optional-dependencies] -jupyterlab = ["qsharp-jupyterlab"] -widgets = ["qsharp-widgets"] -qiskit = ["qiskit>=1.2.2,<3.0.0"] -cirq = ["cirq-core>=1.6.1,<1.7"] -qre = ["cirq-core==1.6.1,<1.7", "pandas>=2.1", "ply>=3.11", "pyqir>=0.12.3,<0.13"] +jupyterlab = ["qdk[jupyter]"] +widgets = ["qdk[jupyter]"] +jupyter = ["qdk[jupyter]"] +qiskit = ["qdk[qiskit]"] +cirq = ["qdk[cirq]"] +qre = ["qdk[qre]"] +azure = ["qdk[azure]"] +applications = ["qdk[applications]"] [build-system] -requires = ["maturin ~= 1.10.2"] -build-backend = "maturin" +requires = ["setuptools>=64", "wheel"] +build-backend = "setuptools.build_meta" -[tool.maturin] -module-name = "qsharp._native" +[tool.setuptools.packages.find] +where = ["."] +exclude = ["build*"] diff --git a/source/pip/qsharp/__init__.py b/source/pip/qsharp/__init__.py index 80432d977e..b07108b4f7 100644 --- a/source/pip/qsharp/__init__.py +++ b/source/pip/qsharp/__init__.py @@ -1,8 +1,36 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from . import telemetry_events -from ._qsharp import ( +"""Deprecated: The ``qsharp`` package has been replaced by ``qdk``. + +All functionality previously available in ``qsharp`` is now provided by the +``qdk`` package. This package is a thin compatibility shim that re-exports the +``qdk`` public API so that existing code continues to work. + +To migrate, replace ``import qsharp`` with ``from qdk import qsharp`` or +``import qdk`` and use the ``qdk.*`` namespace directly. +""" + +import warnings as _warnings + +_warnings.warn( + "The 'qsharp' package is deprecated and will be removed in a future release. " + "Please use the 'qdk' package instead. " + "See https://github.com/microsoft/qdk for migration guidance.", + DeprecationWarning, + stacklevel=2, +) + +# Re-export the full public API from qdk so that existing code keeps working. +from qdk._types import ( + StateDump, + ShotResult, + PauliNoise, + DepolarizingNoise, + BitFlipNoise, + PhaseFlipNoise, +) +from qdk._interpreter import ( init, eval, run, @@ -14,23 +42,25 @@ set_classical_seed, dump_machine, dump_circuit, - StateDump, - ShotResult, - PauliNoise, - DepolarizingNoise, - BitFlipNoise, - PhaseFlipNoise, +) + +from qdk._native import ( + Result, + Pauli, + QSharpError, + TargetProfile, + estimate_custom, CircuitGenerationMethod, ) -telemetry_events.on_import() +from qdk import telemetry_events -from ._native import Result, Pauli, QSharpError, TargetProfile, estimate_custom +telemetry_events.on_import() # IPython notebook specific features try: if __IPYTHON__: # type: ignore - from ._ipython import register_magic + from qdk._ipython import register_magic register_magic() except NameError: diff --git a/source/pip/qsharp/_adaptive_bytecode.py b/source/pip/qsharp/_adaptive_bytecode.py index aa244fc59c..4a5193e367 100644 --- a/source/pip/qsharp/_adaptive_bytecode.py +++ b/source/pip/qsharp/_adaptive_bytecode.py @@ -1,130 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -"""Shared opcode constants for the Adaptive Profile QIR bytecode interpreter. - -These constants define the bytecode encoding used by the Python AdaptiveProfilePass -(emitter) and the Rust GPU receiver. Values must stay in sync with the Rust -``bytecode.rs`` module and the WGSL interpreter. - -Opcode word layout:: - - bits [7:0] = primary opcode - bits [15:8] = sub-opcode / condition code - bits [23:16] = flags - -Compose via bitwise OR: ``opcode | (sub << 8) | flag`` -Example: ``OP_ICMP | (ICMP_SLE << 8) | FLAG_SRC1_IMM`` -""" - -# ── Flags (pre-shifted to bit 16+) ────────────────────────────────────────── -FLAG_DST_IMM = 1 << 18 # dst field is an immediate value, not a register -FLAG_SRC0_IMM = 1 << 16 # src0 field is an immediate value, not a register -FLAG_SRC1_IMM = 1 << 17 # src1 field is an immediate value, not a register -FLAG_AUX0_IMM = 1 << 19 # aux0 field is an immediate value, not a register -FLAG_AUX1_IMM = 1 << 20 # aux1 field is an immediate value, not a register -FLAG_AUX2_IMM = 1 << 21 # aux2 field is an immediate value, not a register -FLAG_AUX3_IMM = 1 << 22 # aux3 field is an immediate value, not a register - -FLAG_FLOAT = 1 << 23 # operation uses float semantics - - -# ── Control Flow ───────────────────────────────────────────────────────────── -OP_NOP = 0x00 -OP_RET = 0x02 -OP_JUMP = 0x04 -OP_BRANCH = 0x05 -OP_SWITCH = 0x06 -OP_CALL = 0x07 -OP_CALL_RETURN = 0x08 - -# ── Quantum ────────────────────────────────────────────────────────────────── -OP_QUANTUM_GATE = 0x10 -OP_MEASURE = 0x11 -OP_RESET = 0x12 -OP_READ_RESULT = 0x13 -OP_RECORD_OUTPUT = 0x14 -OP_READ_LOSS = 0x15 - -# ── Integer Arithmetic ─────────────────────────────────────────────────────── -OP_ADD = 0x20 -OP_SUB = 0x21 -OP_MUL = 0x22 -OP_UDIV = 0x23 -OP_SDIV = 0x24 -OP_UREM = 0x25 -OP_SREM = 0x26 - -# ── Bitwise / Shift ───────────────────────────────────────────────────────── -OP_AND = 0x28 -OP_OR = 0x29 -OP_XOR = 0x2A -OP_SHL = 0x2B -OP_LSHR = 0x2C -OP_ASHR = 0x2D - -# ── Comparison ─────────────────────────────────────────────────────────────── -OP_ICMP = 0x30 -OP_FCMP = 0x31 - -# ── Float Arithmetic ───────────────────────────────────────────────────────── -OP_FADD = 0x38 -OP_FSUB = 0x39 -OP_FMUL = 0x3A -OP_FDIV = 0x3B - -# ── Type Conversion ────────────────────────────────────────────────────────── -OP_ZEXT = 0x40 -OP_SEXT = 0x41 -OP_TRUNC = 0x42 -OP_FPEXT = 0x43 -OP_FPTRUNC = 0x44 -OP_INTTOPTR = 0x45 -OP_FPTOSI = 0x46 -OP_SITOFP = 0x47 - -# ── SSA / Data Movement ───────────────────────────────────────────────────── -OP_PHI = 0x50 -OP_SELECT = 0x51 -OP_MOV = 0x52 -OP_CONST = 0x53 - -# ── ICmp condition codes (sub-opcode, placed in bits[15:8] via << 8) ───────── -# Reference: https://llvm.org/docs/LangRef.html#icmp-instruction -ICMP_EQ = 0 -ICMP_NE = 1 -ICMP_SLT = 2 -ICMP_SLE = 3 -ICMP_SGT = 4 -ICMP_SGE = 5 -ICMP_ULT = 6 -ICMP_ULE = 7 -ICMP_UGT = 8 -ICMP_UGE = 9 - -# ── FCmp condition codes ───────────────────────────────────────────────────── -# Reference: https://llvm.org/docs/LangRef.html#fcmp-instruction -FCMP_FALSE = 0 -FCMP_OEQ = 1 -FCMP_OGT = 2 -FCMP_OGE = 3 -FCMP_OLT = 4 -FCMP_OLE = 5 -FCMP_ONE = 6 -FCMP_ORD = 7 -FCMP_UNO = 8 -FCMP_UEQ = 9 -FCMP_UGT = 10 -FCMP_UGE = 11 -FCMP_ULT = 12 -FCMP_ULE = 13 -FCMP_UNE = 14 -FCMP_TRUE = 15 - -# ── Register type tags ─────────────────────────────────────────────────────── -REG_TYPE_BOOL = 0 -REG_TYPE_I32 = 1 -REG_TYPE_I64 = 2 -REG_TYPE_F32 = 3 -REG_TYPE_F64 = 4 -REG_TYPE_PTR = 5 +# Deprecation shim – delegates to qdk._adaptive_bytecode +from qdk._adaptive_bytecode import * diff --git a/source/pip/qsharp/_adaptive_pass.py b/source/pip/qsharp/_adaptive_pass.py index 11d89fcf0e..883f58b136 100644 --- a/source/pip/qsharp/_adaptive_pass.py +++ b/source/pip/qsharp/_adaptive_pass.py @@ -1,1050 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -"""AdaptiveProfilePass: walks Adaptive Profile QIR and emits the intermediate -format consumed by Rust. - -Unlike ``AggregateGatesPass`` (which subclasses ``pyqir.QirModuleVisitor`` and -only dispatches CALL instructions), this pass iterates basic blocks and -instructions directly so it can handle *all* LLVM IR opcodes required by the -Adaptive Profile specification. -""" - -from __future__ import annotations -from dataclasses import dataclass, astuple -from enum import Enum -import pyqir -import struct -from typing import Any, Dict, List, Optional, Tuple, TypeAlias, cast -from ._adaptive_bytecode import * - - -class Bytecode(Enum): - Bit32 = 32 - Bit64 = 64 - - -# --------------------------------------------------------------------------- -# Gate name → OpID mapping (must match shader_types.rs OpID enum) -# --------------------------------------------------------------------------- - -GATE_MAP: Dict[str, int] = { - "reset": 1, - "x": 2, - "y": 3, - "z": 4, - "h": 5, - "s": 6, - "s__adj": 7, - "t": 8, - "t__adj": 9, - "sx": 10, - "sx__adj": 11, - "rx": 12, - "ry": 13, - "rz": 14, - "cnot": 15, - "cx": 15, - "cz": 16, - "cy": 29, - "rxx": 17, - "ryy": 18, - "rzz": 19, - "ccx": 20, - "m": 21, - "mz": 21, - "mresetz": 22, - "swap": 24, - "move": 28, -} - -# Gates that take a result ID as a second argument -MEASURE_GATES = {"m", "mz", "mresetz"} - -# Gates that reset a qubit (single qubit argument, no result) -RESET_GATES = {"reset"} - -# Rotation gates that take an angle parameter as first argument -ROTATION_GATES = {"rx", "ry", "rz", "rxx", "ryy", "rzz"} - -# Single-qubit gates whose QIR signature carries device-specific extra -# arguments after the qubit pointer (e.g. ``move(qubit, i64, i64)``). The -# extra args are scheduling metadata for hardware backends and are not -# qubit IDs, so we resolve only ``args[0]`` and ignore the rest. -MOVE_GATES = {"move"} - -# --------------------------------------------------------------------------- -# ICmp / FCmp predicate mappings -# --------------------------------------------------------------------------- - -ICMP_MAP = { - pyqir.IntPredicate.EQ: ICMP_EQ, - pyqir.IntPredicate.NE: ICMP_NE, - pyqir.IntPredicate.SLT: ICMP_SLT, - pyqir.IntPredicate.SLE: ICMP_SLE, - pyqir.IntPredicate.SGT: ICMP_SGT, - pyqir.IntPredicate.SGE: ICMP_SGE, - pyqir.IntPredicate.ULT: ICMP_ULT, - pyqir.IntPredicate.ULE: ICMP_ULE, - pyqir.IntPredicate.UGT: ICMP_UGT, - pyqir.IntPredicate.UGE: ICMP_UGE, -} - -FCMP_MAP = { - pyqir.FloatPredicate.FALSE: FCMP_FALSE, - pyqir.FloatPredicate.OEQ: FCMP_OEQ, - pyqir.FloatPredicate.OGT: FCMP_OGT, - pyqir.FloatPredicate.OGE: FCMP_OGE, - pyqir.FloatPredicate.OLT: FCMP_OLT, - pyqir.FloatPredicate.OLE: FCMP_OLE, - pyqir.FloatPredicate.ONE: FCMP_ONE, - pyqir.FloatPredicate.ORD: FCMP_ORD, - pyqir.FloatPredicate.UNO: FCMP_UNO, - pyqir.FloatPredicate.UEQ: FCMP_UEQ, - pyqir.FloatPredicate.UGT: FCMP_UGT, - pyqir.FloatPredicate.UGE: FCMP_UGE, - pyqir.FloatPredicate.ULT: FCMP_ULT, - pyqir.FloatPredicate.ULE: FCMP_ULE, - pyqir.FloatPredicate.UNE: FCMP_UNE, - pyqir.FloatPredicate.TRUE: FCMP_TRUE, -} - - -@dataclass -class AdaptiveProgram: - num_qubits: int - num_results: int - num_registers: int - entry_block: int - blocks: List[Block] - instructions: List[Instruction] - quantum_ops: List[QuantumOp] - functions: List[Function] - phi_entries: List[PhiNodeEntry] - switch_cases: List[SwitchCase] - call_args: List[CallArg] - labels: List[Label] - register_types: List[RegisterType] - - def as_dict(self): - """ - Transforms the program to a dictionary, and each of - the helper dataclasses to a tuple. This format is intended - to be used in the FFI between Python and Rust. - """ - return { - "num_qubits": self.num_qubits, - "num_results": self.num_results, - "num_registers": self.num_registers, - "entry_block": self.entry_block, - "blocks": [astuple(x) for x in self.blocks], - "instructions": [astuple(x) for x in self.instructions], - "quantum_ops": [astuple(x) for x in self.quantum_ops], - "functions": [astuple(x) for x in self.functions], - "phi_entries": [astuple(x) for x in self.phi_entries], - "switch_cases": [astuple(x) for x in self.switch_cases], - "call_args": self.call_args, - "labels": self.labels, - "register_types": self.register_types, - } - - -@dataclass -class Block: - block_id: int - instr_offset: int - instr_count: int - - -@dataclass -class Instruction: - opcode: int - dst: int - src0: int - src1: int - aux0: int - aux1: int - aux2: int - aux3: int - - -@dataclass -class QuantumOp: - op_id: int - q1: int - q2: int - q3: int - # ``angle`` is stored as the raw bit pattern of an IEEE-754 float - # (encoded via ``encode_float_as_bits``) so it can be packed into the - # same integer-typed FFI table as the qubit indices. The Rust side - # reinterprets these bits as f32/f64 depending on the bytecode width. - # - # This also follows the same pattern in which floats are encoded as ints - # in the ``Instruction`` class. - angle: int - - -@dataclass -class Function: - func_entry_block: int - num_params: int - param_base: int - - -@dataclass -class PhiNodeEntry: - block_id: int - val_reg: int - - -@dataclass -class SwitchCase: - case_val: int - target_block: int - - -# OpID for correlated noise (must match shader_types.rs OpID::CorrelatedNoise) -CORRELATED_NOISE_OP_ID = 131 - -CallArg: TypeAlias = int -Label: TypeAlias = str -RegisterType: TypeAlias = int - - -@dataclass -class IntOperand: - val: int - bits: int - - def __post_init__(self): - # Mask to the appropriate word-width so negative Python ints and - # wider-than-target constants become their two's-complement - # representation at the target bit width - # (e.g. -7 → 0xFFFFFFF9 for 32-bit, 0xFFFFFFFFFFFFFFF9 for 64-bit). - # - # Note: we have no way to tell if a negative number, represented by - # pyqir as an u64 is an overflow or just a negative number. - # therefore we don't perform overflow checks here, and instead - # default to a wrapping behavior. - mask = (1 << self.bits) - 1 - self.val = self.val & mask - - -class FloatOperand: - def __init__(self, val: float, bytecode_kind: Bytecode) -> None: - self.val: int = encode_float_as_bits(val, bytecode_kind) - - -@dataclass -class Reg: - val: int # index in the registers table - - -def is_immediate(arg) -> bool: - return isinstance(arg, (IntOperand, FloatOperand)) - - -def prepare_immediate_flags( - *, dst=None, src0=None, src1=None, aux0=None, aux1=None, aux2=None, aux3=None -): - flags = 0 - if is_immediate(dst): - flags |= FLAG_DST_IMM - if is_immediate(src0): - flags |= FLAG_SRC0_IMM - if is_immediate(src1): - flags |= FLAG_SRC1_IMM - if is_immediate(aux0): - flags |= FLAG_AUX0_IMM - if is_immediate(aux1): - flags |= FLAG_AUX1_IMM - if is_immediate(aux2): - flags |= FLAG_AUX2_IMM - if is_immediate(aux3): - flags |= FLAG_AUX3_IMM - return flags - - -def unwrap_operands( - dst, src0, src1, aux0, aux1, aux2, aux3 -) -> Tuple[int, int, int, int, int, int, int]: - if not isinstance(dst, int): - dst = dst.val - if not isinstance(src0, int): - src0 = src0.val - if not isinstance(src1, int): - src1 = src1.val - if not isinstance(aux0, int): - aux0 = aux0.val - if not isinstance(aux1, int): - aux1 = aux1.val - if not isinstance(aux2, int): - aux2 = aux2.val - if not isinstance(aux3, int): - aux3 = aux3.val - return (dst, src0, src1, aux0, aux1, aux2, aux3) - - -def encode_float_as_bits(val: float, bytecode_kind: Bytecode) -> int: - if bytecode_kind == Bytecode.Bit32: - return struct.unpack(" AdaptiveProgram: - """Process module and return the AdaptiveProgram. - - :param mod: The QIR module to process. - :param noise: Optional NoiseConfig. When provided, noise intrinsic calls - are resolved to correlated noise ops using the intrinsics table. - :param noise_intrinsics: Optional dict mapping noise intrinsic callee names - to noise table IDs. Takes precedence over ``noise`` if both are given. - :return: The processed adaptive program. - :rtype: AdaptiveProgram - """ - if mod.get_flag("arrays"): - raise ValueError("QIR arrays are not currently supported.") - - if noise_intrinsics is not None: - self._noise_intrinsics = noise_intrinsics - elif noise is not None: - # Build {name: table_id} mapping from the NoiseConfig intrinsics - intrinsics = noise.intrinsics - self._noise_intrinsics = {} - for callee_name in mod.functions: - name = callee_name.name - if name in intrinsics: - self._noise_intrinsics[name] = intrinsics.get_intrinsic_id(name) - - errors = mod.verify() - if errors is not None: - raise ValueError(f"Module verification failed: {errors}") - - # Pass 1: Assign block IDs and function IDs for all defined functions - for func in mod.functions: - if len(func.basic_blocks) > 0: - self._assign_function(func) - - # Pass 2: Walk instructions and emit encoding - for func in mod.functions: - if len(func.basic_blocks) > 0: - self._walk_function(func) - - entry_func = next(filter(pyqir.is_entry_point, mod.functions)) - num_qubits = pyqir.required_num_qubits(entry_func) - num_results = pyqir.required_num_results(entry_func) - assert isinstance(num_qubits, int) - assert isinstance(num_results, int) - - return AdaptiveProgram( - num_qubits=num_qubits, - num_results=num_results, - num_registers=self._next_reg, - entry_block=self._block_to_id[entry_func.basic_blocks[0]], - blocks=self.blocks, - instructions=self.instructions, - quantum_ops=self.quantum_ops, - functions=self.functions, - phi_entries=self.phi_entries, - switch_cases=self.switch_cases, - call_args=self.call_args, - labels=self.labels, - register_types=self.register_types, - ) - - # ------------------------------------------------------------------ - # Register allocation - # ------------------------------------------------------------------ - - def _alloc_reg(self, value: Any, type_tag: int) -> Reg: - """Allocate a new register for `value` and record its type. - - If `value` was already pre-allocated (e.g. as a forward reference from - a phi node), return the existing register instead of allocating a new - one. - """ - if value is not None and value in self._value_to_reg: - return self._value_to_reg[value] - reg = Reg(self._next_reg) - self._next_reg += 1 - if value is not None: - self._value_to_reg[value] = reg - self.register_types.append(type_tag) - return reg - - # ------------------------------------------------------------------ - # Instruction emission - # ------------------------------------------------------------------ - - def _emit( - self, - opcode: int, - *, - dst: int | IntOperand | FloatOperand | Reg = 0, - src0: int | IntOperand | FloatOperand | Reg = 0, - src1: int | IntOperand | FloatOperand | Reg = 0, - aux0: int | IntOperand | FloatOperand | Reg = 0, - aux1: int | IntOperand | FloatOperand | Reg = 0, - aux2: int | IntOperand | FloatOperand | Reg = 0, - aux3: int | IntOperand | FloatOperand | Reg = 0, - ) -> None: - imm_flags = prepare_immediate_flags( - dst=dst, src0=src0, src1=src1, aux0=aux0, aux1=aux1, aux2=aux2, aux3=aux3 - ) - (dst, src0, src1, aux0, aux1, aux2, aux3) = unwrap_operands( - dst, src0, src1, aux0, aux1, aux2, aux3 - ) - ins = Instruction(opcode | imm_flags, dst, src0, src1, aux0, aux1, aux2, aux3) - self.instructions.append(ins) - - def _emit_quantum_op( - self, - op_id: int, - q1: int = 0, - q2: int = 0, - q3: int = 0, - angle: int = 0, - ) -> int: - idx = self._next_qop - self._next_qop += 1 - qop = QuantumOp(op_id, q1, q2, q3, angle) - self.quantum_ops.append(qop) - return idx - - # ------------------------------------------------------------------ - # Operand resolution - # ------------------------------------------------------------------ - - def _resolve_operand(self, value: pyqir.Value) -> IntOperand | FloatOperand | Reg: - """Resolve a pyqir Value to a register index. - - If `value` is an already-assigned SSA register, return its index. - If `value` is an integer constant, allocate a register and emit - ``OP_CONST`` to materialise it. - """ - if value in self._value_to_reg: - return self._value_to_reg[value] - - if isinstance(value, pyqir.IntConstant): - val = value.value - return IntOperand(val, self._int_bits) - - if isinstance(value, pyqir.FloatConstant): - val = value.value - return FloatOperand(val, self._bytecode_kind) - - # Forward reference (e.g. phi incoming from a later block). - # Pre-allocate a register; the defining instruction will reuse it - # via _alloc_reg's dedup check. - if isinstance(value, pyqir.Instruction): - return self._alloc_reg(value, self._type_tag(value.type)) - - # Constant expressions (e.g. inttoptr (i64 N to ptr)). - if isinstance(value, pyqir.Constant): - # Try extracting as a qubit/result pointer constant. - pid = pyqir.ptr_id(value) - if pid is not None: - return IntOperand(pid, self._int_bits) - # Null pointer - if value.is_null: - reg = self._alloc_reg(value, REG_TYPE_PTR) - self._emit(OP_CONST | FLAG_SRC0_IMM, dst=reg.val, src0=0) - return reg - - raise ValueError(f"Cannot resolve operand: {type(value).__name__}") - - def _type_tag(self, ty: Any) -> int: - """Map a pyqir Type to a register type tag.""" - if isinstance(ty, pyqir.IntType): - w = ty.width - if w == 1: - return REG_TYPE_BOOL - if w <= 32: - return REG_TYPE_I32 - return REG_TYPE_I64 - if isinstance(ty, pyqir.PointerType): - return REG_TYPE_PTR - if ty.is_double: - return REG_TYPE_F64 - # Remaining floating-point types (e.g. float/f32) - return REG_TYPE_F32 - - # ------------------------------------------------------------------ - # Binary / unary helpers - # ------------------------------------------------------------------ - - def _emit_binary(self, opcode: int, instr: Any) -> None: - """Emit a binary arithmetic/bitwise instruction.""" - dst = self._alloc_reg(instr, self._type_tag(instr.type)) - src0 = self._resolve_operand(instr.operands[0]) - src1 = self._resolve_operand(instr.operands[1]) - self._emit(opcode, dst=dst, src0=src0, src1=src1) - - def _emit_unary(self, opcode: int, instr: Any) -> None: - """Emit a unary conversion instruction.""" - dst = self._alloc_reg(instr, self._type_tag(instr.type)) - src0 = self._resolve_operand(instr.operands[0]) - self._emit(opcode, dst=dst, src0=src0) - - def _emit_sext(self, instr: Any) -> None: - """Emit OP_SEXT with source bit width in aux0.""" - dst = self._alloc_reg(instr, self._type_tag(instr.type)) - src0 = self._resolve_operand(instr.operands[0]) - src_type = instr.operands[0].type - src_bits = src_type.width if isinstance(src_type, pyqir.IntType) else 32 - self._emit(OP_SEXT, dst=dst, src0=src0, aux0=src_bits) - - # ------------------------------------------------------------------ - # Function assignment (Pass 1) - # ------------------------------------------------------------------ - - def _assign_function(self, func: pyqir.Function) -> None: - """Assign block IDs and function IDs for a function.""" - if not pyqir.is_entry_point(func) and func.name not in self._func_to_id: - func_id = len(self._func_to_id) - self._func_to_id[func.name] = func_id - for block in func.basic_blocks: - self._block_to_id[block] = self._next_block - self._next_block += 1 - - # ------------------------------------------------------------------ - # Function walking (Pass 2) - # ------------------------------------------------------------------ - - def _walk_function(self, func: pyqir.Function) -> None: - """Walk all blocks and instructions in a function, emitting bytecode.""" - self._current_func_is_entry = pyqir.is_entry_point(func) - - # For non-entry functions, register parameters as registers - if not self._current_func_is_entry: - param_base = self._next_reg - for param in func.params: - self._alloc_reg( - param, REG_TYPE_PTR - ) # params are pointers (%Qubit*, %Result*) - # Record function entry in the function table - if func.name in self._func_to_id: - func_entry_block = self._block_to_id[func.basic_blocks[0]] - f = Function(func_entry_block, len(func.params), param_base) - self.functions.append(f) - - for block in func.basic_blocks: - block_id = self._block_to_id[block] - instr_offset = len(self.instructions) - for instr in block.instructions: - self._on_instruction(instr) - # NOTE: block.terminator is already included in block.instructions - # in pyqir, so we do NOT separately process it. - instr_count = len(self.instructions) - instr_offset - blk = Block(block_id, instr_offset, instr_count) - self.blocks.append(blk) - - # ------------------------------------------------------------------ - # Instruction dispatch - # ------------------------------------------------------------------ - - def _on_instruction(self, instr: pyqir.Instruction) -> None: - """Dispatch a single instruction by opcode.""" - match instr.opcode: - case pyqir.Opcode.CALL: - self._emit_call(cast(pyqir.Call, instr)) - case pyqir.Opcode.PHI: - self._emit_phi(cast(pyqir.Phi, instr)) - case pyqir.Opcode.ICMP: - self._emit_icmp(cast(pyqir.ICmp, instr)) - case pyqir.Opcode.FCMP: - self._emit_fcmp(cast(pyqir.FCmp, instr)) - case pyqir.Opcode.SWITCH: - self._emit_switch(cast(pyqir.Switch, instr)) - case pyqir.Opcode.BR: - self._emit_branch(instr) - case pyqir.Opcode.RET: - self._emit_ret(instr) - case pyqir.Opcode.SELECT: - self._emit_select(instr) - case pyqir.Opcode.ADD: - self._emit_binary(OP_ADD, instr) - case pyqir.Opcode.SUB: - self._emit_binary(OP_SUB, instr) - case pyqir.Opcode.MUL: - self._emit_binary(OP_MUL, instr) - case pyqir.Opcode.UDIV: - self._emit_binary(OP_UDIV, instr) - case pyqir.Opcode.SDIV: - self._emit_binary(OP_SDIV, instr) - case pyqir.Opcode.UREM: - self._emit_binary(OP_UREM, instr) - case pyqir.Opcode.SREM: - self._emit_binary(OP_SREM, instr) - case pyqir.Opcode.AND: - self._emit_binary(OP_AND, instr) - case pyqir.Opcode.OR: - self._emit_binary(OP_OR, instr) - case pyqir.Opcode.XOR: - self._emit_binary(OP_XOR, instr) - case pyqir.Opcode.SHL: - self._emit_binary(OP_SHL, instr) - case pyqir.Opcode.LSHR: - self._emit_binary(OP_LSHR, instr) - case pyqir.Opcode.ASHR: - self._emit_binary(OP_ASHR, instr) - case pyqir.Opcode.ZEXT: - self._emit_unary(OP_ZEXT, instr) - case pyqir.Opcode.SEXT: - self._emit_sext(instr) - case pyqir.Opcode.TRUNC: - self._emit_unary(OP_TRUNC, instr) - case pyqir.Opcode.FADD: - self._emit_binary(OP_FADD | FLAG_FLOAT, instr) - case pyqir.Opcode.FSUB: - self._emit_binary(OP_FSUB | FLAG_FLOAT, instr) - case pyqir.Opcode.FMUL: - self._emit_binary(OP_FMUL | FLAG_FLOAT, instr) - case pyqir.Opcode.FDIV: - self._emit_binary(OP_FDIV | FLAG_FLOAT, instr) - case pyqir.Opcode.FP_EXT: - self._emit_unary(OP_FPEXT | FLAG_FLOAT, instr) - case pyqir.Opcode.FP_TRUNC: - self._emit_unary(OP_FPTRUNC | FLAG_FLOAT, instr) - case pyqir.Opcode.FP_TO_SI: - self._emit_unary(OP_FPTOSI, instr) - case pyqir.Opcode.SI_TO_FP: - self._emit_unary(OP_SITOFP | FLAG_FLOAT, instr) - case pyqir.Opcode.INT_TO_PTR: - self._emit_inttoptr(instr) - case _: - raise ValueError(f"Unsupported instruction: {instr.opcode}") - - # ------------------------------------------------------------------ - # Call dispatch - # ------------------------------------------------------------------ - - def _emit_call(self, call: pyqir.Call) -> None: - """Dispatch a CALL instruction based on callee name.""" - callee = call.callee.name - - match callee: - case "__quantum__qis__read_result__body" | "__quantum__rt__read_result": - dst = self._alloc_reg(call, REG_TYPE_BOOL) - result_reg = self._resolve_result_operand(call.args[0]) - self._emit(OP_READ_RESULT, dst=dst, src0=result_reg) - case "__quantum__rt__result_record_output": - result_reg = self._resolve_result_operand(call.args[0]) - label_str = self._extract_label(call.args[1]) - label_idx = len(self.labels) - self.labels.append(label_str) - self._emit(OP_RECORD_OUTPUT, src0=result_reg, aux0=label_idx) - case "__quantum__rt__array_record_output": - # Record structure output — pass through as-is for output formatting - count = ( - call.args[0].value - if isinstance(call.args[0], pyqir.IntConstant) - else 0 - ) - label_str = self._extract_label(call.args[1]) - label_idx = len(self.labels) - self.labels.append(label_str) - self._emit( - OP_RECORD_OUTPUT, src0=count, aux0=label_idx, aux1=1 - ) # aux1=1 -> array - case "__quantum__rt__tuple_record_output": - count = ( - call.args[0].value - if isinstance(call.args[0], pyqir.IntConstant) - else 0 - ) - label_str = self._extract_label(call.args[1]) - label_idx = len(self.labels) - self.labels.append(label_str) - self._emit( - OP_RECORD_OUTPUT, src0=count, aux0=label_idx, aux1=2 - ) # aux1=2 -> tuple - case "__quantum__rt__bool_record_output": - # Bool record output - pass through - src = self._resolve_operand(call.args[0]) - label_str = self._extract_label(call.args[1]) - label_idx = len(self.labels) - self.labels.append(label_str) - self._emit( - OP_RECORD_OUTPUT, src0=src, aux0=label_idx, aux1=3 - ) # aux1=3 -> bool - case "__quantum__rt__int_record_output": - src = self._resolve_operand(call.args[0]) - label_str = self._extract_label(call.args[1]) - label_idx = len(self.labels) - self.labels.append(label_str) - self._emit( - OP_RECORD_OUTPUT, src0=src, aux0=label_idx, aux1=4 - ) # aux1=4 -> int - case ( - "__quantum__rt__initialize" - | "__quantum__rt__begin_parallel" - | "__quantum__rt__end_parallel" - | "__quantum__qis__barrier__body" - ): - pass # No-op - case "__quantum__rt__read_loss": - # Allocate a bool register and emit OP_READ_LOSS so the runtime - # can ask the simulator whether the given result was produced - # by measuring a lost qubit. Programs may branch on this value. - dst = self._alloc_reg(call, REG_TYPE_BOOL) - result_reg = self._resolve_result_operand(call.args[0]) - self._emit(OP_READ_LOSS, dst=dst, src0=result_reg) - case _ if callee.startswith("__quantum__qis__"): - self._emit_quantum_call(call) - case _ if callee in self._func_to_id: - self._emit_ir_function_call(call) - case _ if "qdk_noise" in call.callee.attributes.func: - # Check if this is a noise intrinsic (custom gate with qdk_noise attribute) - self._emit_noise_intrinsic_call(call) - case _: - raise ValueError(f"Unsupported call: {callee}") - - # ------------------------------------------------------------------ - # Quantum call dispatch - # ------------------------------------------------------------------ - - def _resolve_qubit_operands( - self, args: List[pyqir.Value] - ) -> Tuple[IntOperand | Reg, IntOperand | Reg, IntOperand | Reg]: - qs: List[IntOperand | Reg] = [ - IntOperand(0, self._int_bits), - IntOperand(0, self._int_bits), - IntOperand(0, self._int_bits), - ] - for i, arg in enumerate(args): - qs[i] = self._resolve_qubit_operand(arg) - return (qs[0], qs[1], qs[2]) - - def _resolve_qubit_operand(self, arg: pyqir.Value) -> IntOperand | Reg: - a = self._resolve_operand(arg) - assert isinstance(a, (IntOperand, Reg)) - return a - - def _resolve_result_operand(self, arg: pyqir.Value) -> IntOperand | Reg: - a = self._resolve_operand(arg) - assert isinstance(a, (IntOperand, Reg)) - return a - - def _resolve_angle_operand(self, arg: pyqir.Value) -> FloatOperand | Reg: - a = self._resolve_operand(arg) - assert isinstance(a, (FloatOperand, Reg)) - return a - - def _emit_quantum_call(self, call: pyqir.Call) -> None: - """Emit a quantum gate, measure, or reset from a ``__quantum__qis__*`` call.""" - callee_name = call.callee.name - gate_name = callee_name.replace("__quantum__qis__", "").replace("__body", "") - op_id = GATE_MAP[gate_name] - if gate_name in MEASURE_GATES: - q = self._resolve_qubit_operand(call.args[0]) - r = self._resolve_result_operand(call.args[1]) - qop_idx = self._emit_quantum_op(op_id, q.val, r.val) - self._emit( - OP_MEASURE, - aux0=qop_idx, - aux1=q, - aux2=r, - ) - return - if gate_name in RESET_GATES: - q = self._resolve_qubit_operand(call.args[0]) - qop_idx = self._emit_quantum_op(op_id, q.val) - self._emit( - OP_RESET, - aux0=qop_idx, - aux1=q, - ) - return - if gate_name in MOVE_GATES: - # ``move(qubit, i64, i64)``: only the first arg is a qubit; the - # remaining args are device-specific scheduling metadata that - # the simulator ignores. Emit a single-qubit OP_QUANTUM_GATE so - # the runtime invokes ``Simulator::mov`` (which applies the - # configured ``noise.mov`` faults to that qubit). - q1, q2, q3 = self._resolve_qubit_operands([call.args[0]]) - angle = FloatOperand(0.0, self._bytecode_kind) - qop_idx = self._emit_quantum_op(op_id, q1.val, q2.val, q3.val, angle.val) - self._emit( - OP_QUANTUM_GATE, - src0=angle, - aux0=qop_idx, - aux1=q1, - aux2=q2, - aux3=q3, - ) - return - if gate_name in ROTATION_GATES: - qubit_arg_offset = 1 - angle = self._resolve_angle_operand(call.args[0]) - else: - qubit_arg_offset = 0 - angle = FloatOperand(0.0, self._bytecode_kind) - qubit_arg_offset = 1 if gate_name in ROTATION_GATES else 0 - q1, q2, q3 = self._resolve_qubit_operands(call.args[qubit_arg_offset:]) - qop_idx = self._emit_quantum_op(op_id, q1.val, q2.val, q3.val, angle.val) - self._emit( - OP_QUANTUM_GATE, - src0=angle, - aux0=qop_idx, - aux1=q1, - aux2=q2, - aux3=q3, - ) - - def _emit_noise_intrinsic_call(self, call: pyqir.Call) -> None: - """Emit a noise intrinsic call. - - When a noise config is provided and the callee is a known intrinsic, - store qubit register indices in ``call_args`` (following the same - pattern as ``_emit_ir_function_call``), then emit a single - ``OP_QUANTUM_GATE`` whose ``aux1`` = qubit count and ``aux2`` = - offset into ``call_args``. The shader reads qubit IDs from - ``call_arg_table`` at runtime, supporting arbitrarily many qubits. - - When no noise config is provided, emit an identity gate (no-op). - """ - callee_name = call.callee.name - if self._noise_intrinsics is not None and callee_name in self._noise_intrinsics: - table_id = self._noise_intrinsics[callee_name] - qubit_count = len(call.args) - # Store qubit register indices in call_args, materializing - # immediates into registers (same pattern as _emit_ir_function_call). - arg_offset = len(self.call_args) - for arg in call.args: - operand = self._resolve_qubit_operand(arg) - if isinstance(operand, Reg): - self.call_args.append(operand.val) - else: - reg = self._alloc_reg(None, REG_TYPE_PTR) - self._emit(OP_MOV | FLAG_SRC0_IMM, dst=reg, src0=operand.val) - self.call_args.append(reg.val) - # QuantumOp stores table_id in q1 and qubit_count in q2. - qop_idx = self._emit_quantum_op( - CORRELATED_NOISE_OP_ID, table_id, qubit_count - ) - self._emit( - OP_QUANTUM_GATE, - aux0=qop_idx, - aux1=IntOperand(qubit_count, self._int_bits), - aux2=IntOperand(arg_offset, self._int_bits), - ) - elif self._noise_intrinsics is not None: - raise ValueError(f"Missing noise intrinsic: {callee_name}") - else: - # No noise config — no-op - pass - - # ------------------------------------------------------------------ - # Control flow emitters - # ------------------------------------------------------------------ - - def _emit_branch(self, instr: pyqir.Instruction) -> None: - """Emit jump or conditional branch.""" - operands = instr.operands - if len(operands) == 1: - # Unconditional: br label %target - target = self._block_to_id[operands[0]] - self._emit(OP_JUMP, dst=target) - else: - # Conditional: br i1 %cond, label %true, label %false - # pyqir operands: [condition, FALSE_block, TRUE_block] - cond_reg = self._resolve_operand(operands[0]) - false_block = self._block_to_id[operands[1]] - true_block = self._block_to_id[operands[2]] - self._emit(OP_BRANCH, src0=cond_reg, aux0=true_block, aux1=false_block) - - def _emit_phi(self, phi_instr: pyqir.Phi) -> None: - """Emit a PHI node with side table entries.""" - dst_reg = self._alloc_reg(phi_instr, self._type_tag(phi_instr.type)) - phi_offset = len(self.phi_entries) - for value, block in phi_instr.incoming: - operand = self._resolve_operand(value) - if isinstance(operand, Reg): - val_reg = operand.val - else: - # Immediate values must be materialized into a register - # because the GPU phi_table stores register indices. - reg = self._alloc_reg(None, self._type_tag(phi_instr.type)) - self._emit(OP_MOV | FLAG_SRC0_IMM, dst=reg, src0=operand.val) - val_reg = reg.val - block_id = self._block_to_id[block] - phi_entry = PhiNodeEntry(block_id, val_reg) - self.phi_entries.append(phi_entry) - count = len(phi_instr.incoming) - self._emit(OP_PHI, dst=dst_reg, aux0=phi_offset, aux1=count) - - def _emit_select(self, instr: pyqir.Instruction) -> None: - """Emit a SELECT instruction.""" - dst = self._alloc_reg(instr, self._type_tag(instr.type)) - cond = self._resolve_operand(instr.operands[0]) - true_val = self._resolve_operand(instr.operands[1]) - false_val = self._resolve_operand(instr.operands[2]) - self._emit(OP_SELECT, dst=dst, src0=cond, aux0=true_val, aux1=false_val) - - def _emit_switch(self, switch_instr: pyqir.Switch) -> None: - """Emit a SWITCH instruction with case table entries. - - NOTE: We use ``operands`` instead of the ``.cond`` / ``.cases`` - helpers because pyqir's ``Switch.cond`` returns a stale ``Function`` - reference when ``mod.functions`` has already been iterated (two-pass - compilation). ``operands`` is not affected by this behavior. - """ - # operands layout: [cond, default_block, case_val0, case_block0, ...] - cond_reg = self._resolve_operand(switch_instr.operands[0]) - default_block = self._block_to_id[switch_instr.default] - case_offset = len(self.switch_cases) - for case_val, block in switch_instr.cases: - target_block = self._block_to_id[block] - switch_case = SwitchCase(case_val.value, target_block) - self.switch_cases.append(switch_case) - case_count = len(switch_instr.cases) - self._emit( - OP_SWITCH, - src0=cond_reg, - aux0=default_block, - aux1=case_offset, - aux2=case_count, - ) - - def _emit_ret(self, instr: Any) -> None: - """Emit RET or CALL_RETURN.""" - if not self._current_func_is_entry: - # Return from IR-defined function - if len(instr.operands) > 0: - ret_reg = self._resolve_operand(instr.operands[0]) - self._emit(OP_CALL_RETURN, src0=ret_reg) - else: - self._emit(OP_CALL_RETURN) - else: - # Return from entry point - if len(instr.operands) > 0: - ret_reg = self._resolve_operand(instr.operands[0]) - self._emit(OP_RET, dst=ret_reg) - else: - # Void return — use immediate 0 as exit code. - self._emit(OP_RET, dst=IntOperand(0, self._int_bits)) - - # ------------------------------------------------------------------ - # Comparison emitters - # ------------------------------------------------------------------ - - def _emit_icmp(self, instr: Any) -> None: - """Emit an integer comparison.""" - cond_code = ICMP_MAP.get(instr.predicate, 0) - dst = self._alloc_reg(instr, REG_TYPE_BOOL) - src0 = self._resolve_operand(instr.operands[0]) - src1 = self._resolve_operand(instr.operands[1]) - self._emit(OP_ICMP | (cond_code << 8), dst=dst, src0=src0, src1=src1) - - def _emit_fcmp(self, instr: Any) -> None: - """Emit a float comparison.""" - cond_code = FCMP_MAP.get(instr.predicate, 0) - dst = self._alloc_reg(instr, REG_TYPE_BOOL) - src0 = self._resolve_operand(instr.operands[0]) - src1 = self._resolve_operand(instr.operands[1]) - self._emit( - OP_FCMP | (cond_code << 8) | FLAG_FLOAT, - dst=dst, - src0=src0, - src1=src1, - ) - - # ------------------------------------------------------------------ - # inttoptr handling - # ------------------------------------------------------------------ - - def _emit_inttoptr(self, instr: Any) -> None: - """Handle ``inttoptr`` — just propagate the source register. - - ``inttoptr i64 %v to %Qubit*`` is a no-op cast; the integer value - is the qubit/result ID. We use OP_MOV to alias the value. - """ - src_operand = instr.operands[0] - src_reg = self._resolve_operand(src_operand) - # Register the inttoptr result as pointing to the same register - dst = self._alloc_reg(instr, REG_TYPE_PTR) - self._emit(OP_MOV, dst=dst, src0=src_reg) - - # ------------------------------------------------------------------ - # IR-defined function call/return - # ------------------------------------------------------------------ - - def _emit_ir_function_call(self, call: Any) -> None: - """Emit OP_CALL for an IR-defined function.""" - func_name = call.callee.name - func_id = self._func_to_id[func_name] - arg_offset = len(self.call_args) - for arg in call.args: - operand = self._resolve_operand(arg) - if isinstance(operand, Reg): - self.call_args.append(operand.val) - else: - # Immediate values must be materialized into a register - # because the GPU call_arg_table stores register indices. - reg = self._alloc_reg(None, REG_TYPE_PTR) - self._emit(OP_MOV | FLAG_SRC0_IMM, dst=reg, src0=operand.val) - self.call_args.append(reg.val) - # Allocate return register if function has non-void return type - if call.type.is_void: - return_reg = void_return(self._bytecode_kind) # no return - else: - return_reg = self._alloc_reg(call, REG_TYPE_I32) - self._emit( - OP_CALL, - dst=return_reg, - aux0=func_id, - aux1=len(call.args), - aux2=arg_offset, - ) - - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ - - def _extract_label(self, value: Any) -> str: - """Extract a label string from a call argument.""" - bs = pyqir.extract_byte_string(value) - if bs is not None: - return bs.decode("utf-8") - return "" +# Deprecation shim – delegates to qdk._adaptive_pass +from qdk._adaptive_pass import * diff --git a/source/pip/qsharp/_device/__init__.py b/source/pip/qsharp/_device/__init__.py index 59041732f4..1b0b1e35a3 100644 --- a/source/pip/qsharp/_device/__init__.py +++ b/source/pip/qsharp/_device/__init__.py @@ -1,8 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from ._device import Device - -__all__ = [ - "Device", -] +# Deprecated: use qdk._device instead. +from qdk._device import * # noqa: F401,F403 diff --git a/source/pip/qsharp/_device/_atom/__init__.py b/source/pip/qsharp/_device/_atom/__init__.py index f58a7ab77f..8cd552d4b2 100644 --- a/source/pip/qsharp/_device/_atom/__init__.py +++ b/source/pip/qsharp/_device/_atom/__init__.py @@ -1,327 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from .._device import Device, Zone, ZoneType -from ..._simulation import NoiseConfig, run_qir_clifford, run_qir_cpu, run_qir_gpu -from ..._native import try_create_gpu_adapter -from ..._qsharp import QirInputData -from ... import telemetry_events - -from typing import List, Literal, Optional -import time - - -class NeutralAtomDevice(Device): - """ - Representation of a neutral atom device quantum computer. - """ - - def __init__( - self, - column_count: int = 40, - register_zone_row_count: int = 25, - interaction_zone_row_count: int = 2, - measurement_zone_row_count: int = 2, - ): - default_layout = ( - column_count == 40 - and register_zone_row_count == 25 - and interaction_zone_row_count == 2 - and measurement_zone_row_count == 2 - ) - telemetry_events.on_neutral_atom_init(default_layout) - - super().__init__( - column_count, - [ - Zone("Register 1", register_zone_row_count, ZoneType.REG), - Zone("Interaction Zone", interaction_zone_row_count, ZoneType.INTER), - Zone("Measurement Zone", measurement_zone_row_count, ZoneType.MEAS), - ], - ) - - def _init_home_locs(self): - # Set up the home locations for qubits in the NeutralAtomDevice layout. - assert len(self.zones) == 3 - assert ( - self.zones[0].type == ZoneType.REG - and self.zones[1].type == ZoneType.INTER - and self.zones[2].type == ZoneType.MEAS - ) - rz1_rows = range(self.zones[0].row_count - 1, -1, -1) - self.home_locs = [] - for row in range(self.zones[0].row_count): - for col in range(self.column_count): - self.home_locs.append((rz1_rows[row], col)) - - def compile( - self, - program: str | QirInputData, - verbose: bool = False, - ) -> QirInputData: - """ - Compile a QIR program for the NeutralAtomDevice device. This includes decomposing gates to the native gate set, - optimizing sequences of single qubit gates, pruning unused functions, and reordering instructions to - enable better scheduling during execution. - - :param program: The QIR program to compile, either as a string or as QirInputData. - :param verbose: If true, print detailed information about each compilation step. - :returns QirInputData: The compiled QIR program. - """ - - from ._optimize import ( - OptimizeSingleQubitGates, - PruneUnusedFunctions, - ) - from ._decomp import ( - DecomposeMultiQubitToCZ, - DecomposeSingleRotationToRz, - DecomposeSingleQubitToRzSX, - ReplaceResetWithMResetZ, - ) - from ._reorder import Reorder - from pyqir import Module, Context - - start_time = time.monotonic() - all_start_time = start_time - telemetry_events.on_neutral_atom_compile() - - name = "" - if isinstance(program, QirInputData): - name = program._name - - if verbose: - print(f"Compiling program {name} for NeutralAtomDevice device...") - - module = Module.from_ir(Context(), str(program)) - if verbose: - end_time = time.monotonic() - print(f" Loaded module in {end_time - start_time:.2f} seconds") - start_time = end_time - - OptimizeSingleQubitGates().run(module) - if verbose: - end_time = time.monotonic() - print( - f" Optimized single qubit gates in {end_time - start_time:.2f} seconds" - ) - start_time = end_time - - DecomposeMultiQubitToCZ().run(module) - if verbose: - end_time = time.monotonic() - print( - f" Decomposed multi-qubit gates to CZ in {end_time - start_time:.2f} seconds" - ) - start_time = end_time - - OptimizeSingleQubitGates().run(module) - if verbose: - end_time = time.monotonic() - print( - f" Optimized single qubit gates in {end_time - start_time:.2f} seconds" - ) - start_time = end_time - - DecomposeSingleRotationToRz().run(module) - if verbose: - end_time = time.monotonic() - print( - f" Decomposed single rotations to Rz in {end_time - start_time:.2f} seconds" - ) - start_time = end_time - - OptimizeSingleQubitGates().run(module) - if verbose: - end_time = time.monotonic() - print( - f" Optimized single qubit gates in {end_time - start_time:.2f} seconds" - ) - start_time = end_time - - DecomposeSingleQubitToRzSX().run(module) - if verbose: - end_time = time.monotonic() - print( - f" Decomposed single qubit gates to Rz and SX in {end_time - start_time:.2f} seconds" - ) - start_time = end_time - - OptimizeSingleQubitGates().run(module) - if verbose: - end_time = time.monotonic() - print( - f" Optimized single qubit gates in {end_time - start_time:.2f} seconds" - ) - start_time = end_time - - ReplaceResetWithMResetZ().run(module) - if verbose: - end_time = time.monotonic() - print( - f" Replaced resets with mresetz in {end_time - start_time:.2f} seconds" - ) - start_time = end_time - - PruneUnusedFunctions().run(module) - if verbose: - end_time = time.monotonic() - print(f" Pruned unused functions in {end_time - start_time:.2f} seconds") - start_time = end_time - - Reorder(self).run(module) - if verbose: - end_time = time.monotonic() - print(f" Reordered instructions in {end_time - start_time:.2f} seconds") - start_time = end_time - - end_time = time.monotonic() - telemetry_events.on_neutral_atom_compile_end((end_time - all_start_time) * 1000) - if verbose: - print( - f"Finished compiling program {name} in {end_time - all_start_time:.2f} seconds" - ) - - return QirInputData(name, str(module)) - - def show_trace(self, qir: str | QirInputData): - """ - Visualize the execution trace of a QIR program on the NeutralAtomDevice device using the Atoms widget. - This includes approximate layout and scheduling of the program to show the parallelism of gates and - movement of qubits during execution. - - :param qir: The QIR program to visualize, either as a string or as QirInputData. - """ - - try: - from qsharp_widgets import Atoms - except ImportError: - raise ImportError( - "The qsharp-widgets package is required for showing atom trace visualization. " - "Please install it via 'pip install \"qdk[jupyter]\"' or 'pip install qsharp-widgets'." - ) - from ._trace import Trace - from ._validate import ValidateNoConditionalBranches - from ._scheduler import Schedule - from pyqir import Module, Context - from IPython.display import display - - start_time = time.monotonic() - telemetry_events.on_neutral_atom_trace() - - # Compile and visualize the trace in one step. - compiled = self.compile(qir) - module = Module.from_ir(Context(), str(compiled)) - ValidateNoConditionalBranches().run(module) - Schedule(self).run(module) - tracer = Trace(self) - tracer.run(module) - display(Atoms(machine_layout=self.get_layout(), trace_data=tracer.trace)) - - end_time = time.monotonic() - telemetry_events.on_neutral_atom_trace_end((end_time - start_time) * 1000) - - def simulate( - self, - qir: str | QirInputData, - shots=1, - noise: NoiseConfig | None = None, - type: Optional[Literal["clifford", "cpu", "gpu"]] = None, - seed: Optional[int] = None, - ) -> List: - """ - Simulate a QIR program on the NeutralAtomDevice device. This includes approximate layout and scheduling of the program - to model the parallelism of gates and movement of qubits during execution. The simulation can optionally - include noise based on a provided noise configuration. - - :param qir: The QIR program to simulate, either as a string or as QirInputData. - :param shots: The number of shots to simulate. Defaults to 1. - :param noise: An optional NoiseConfig to include noise in the simulation. - :param type: The type of simulator to use: - Use `"clifford"` if your QIR only contains Clifford gates and measurements. - Use `"gpu"` if you have a GPU available in your system. - Use `"cpu"` as a fallback option if you don't have a GPU in your system. - If `None` (default), the GPU simulator will be tried first, falling back to - CPU if a suitable GPU device could not be located. - :param seed: An optional random seed for reproducibility. - :return: The results of each shot of the simulation as a list. - """ - - from ._validate import ValidateNoConditionalBranches - from ._scheduler import Schedule - from ._decomp import DecomposeRzAnglesToCliffordGates - from pyqir import Module, Context - - start_time = time.monotonic() - - using_noise = noise is not None - if noise is None: - noise = NoiseConfig() - - # Override t, t_adj, s, s_adj, and z noise if they are unset and rz noise is set. - if noise and not noise.rz.is_noiseless(): - if noise.t.is_noiseless(): - noise.t.x = noise.rz.x - noise.t.y = noise.rz.y - noise.t.z = noise.rz.z - noise.t.loss = noise.rz.loss - if noise.t_adj.is_noiseless(): - noise.t_adj.x = noise.rz.x - noise.t_adj.y = noise.rz.y - noise.t_adj.z = noise.rz.z - noise.t_adj.loss = noise.rz.loss - if noise.s.is_noiseless(): - noise.s.x = noise.rz.x - noise.s.y = noise.rz.y - noise.s.z = noise.rz.z - noise.s.loss = noise.rz.loss - if noise.s_adj.is_noiseless(): - noise.s_adj.x = noise.rz.x - noise.s_adj.y = noise.rz.y - noise.s_adj.z = noise.rz.z - noise.s_adj.loss = noise.rz.loss - if noise.z.is_noiseless(): - noise.z.x = noise.rz.x - noise.z.y = noise.rz.y - noise.z.z = noise.rz.z - noise.z.loss = noise.rz.loss - - compiled = self.compile(qir) - module = Module.from_ir(Context(), str(compiled)) - ValidateNoConditionalBranches().run(module) - Schedule(self).run(module) - - if type is None: - try: - try_create_gpu_adapter() - type = "gpu" - except OSError: - telemetry_events.on_neutral_atom_cpu_fallback() - type = "cpu" - - telemetry_events.on_neutral_atom_simulate(shots, using_noise, type) - - match type: - case "clifford": - DecomposeRzAnglesToCliffordGates().run(module) - result = run_qir_clifford( - str(module), - shots, - noise, - seed, - ) - case "cpu": - result = run_qir_cpu(str(module), shots, noise, seed) - case "gpu": - result = run_qir_gpu(str(module), shots, noise, seed) - case _: - raise ValueError(f"Simulation type {type} is not supported") - - end_time = time.monotonic() - telemetry_events.on_neutral_atom_simulate_end( - (end_time - start_time) * 1000, shots, using_noise, type - ) - return result - - -__all__ = ["NeutralAtomDevice"] +# Deprecated: use qdk._device._atom instead. +from qdk._device._atom import * # noqa: F401,F403 diff --git a/source/pip/qsharp/_device/_atom/_decomp.py b/source/pip/qsharp/_device/_atom/_decomp.py index f51d878119..a839752945 100644 --- a/source/pip/qsharp/_device/_atom/_decomp.py +++ b/source/pip/qsharp/_device/_atom/_decomp.py @@ -1,510 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -from pyqir import ( - FloatConstant, - const, - Function, - FunctionType, - PointerType, - Type, - result, - Context, - Linkage, - QirModuleVisitor, - required_num_results, -) -from math import pi -from ._utils import TOLERANCE - - -class DecomposeMultiQubitToCZ(QirModuleVisitor): - """ - Decomposes all multi-qubit gates to CZ gates and single qubit gates. - """ - - h_func: Function - s_func: Function - sadj_func: Function - t_func: Function - tadj_func: Function - rz_func: Function - cz_func: Function - - def _on_module(self, module): - void = Type.void(module.context) - qubit_ty = PointerType(Type.void(module.context)) - self.double_ty = Type.double(module.context) - # Find or create all the needed functions. - for func in module.functions: - match func.name: - case "__quantum__qis__h__body": - self.h_func = func - case "__quantum__qis__s__body": - self.s_func = func - case "__quantum__qis__s__adj": - self.sadj_func = func - case "__quantum__qis__t__body": - self.t_func = func - case "__quantum__qis__t__adj": - self.tadj_func = func - case "__quantum__qis__rz__body": - self.rz_func = func - case "__quantum__qis__cz__body": - self.cz_func = func - if not hasattr(self, "h_func"): - self.h_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__h__body", - module, - ) - if not hasattr(self, "s_func"): - self.s_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__s__body", - module, - ) - if not hasattr(self, "sadj_func"): - self.sadj_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__s__adj", - module, - ) - if not hasattr(self, "t_func"): - self.t_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__t__body", - module, - ) - if not hasattr(self, "tadj_func"): - self.tadj_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__t__adj", - module, - ) - if not hasattr(self, "rz_func"): - self.rz_func = Function( - FunctionType(void, [self.double_ty, qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__rz__body", - module, - ) - if not hasattr(self, "cz_func"): - self.cz_func = Function( - FunctionType(void, [qubit_ty, qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__cz__body", - module, - ) - super()._on_module(module) - - def _on_qis_ccx(self, call, ctrl1, ctrl2, target): - self.builder.insert_before(call) - self.builder.call(self.h_func, [target]) - self.builder.call(self.tadj_func, [ctrl1]) - self.builder.call(self.tadj_func, [ctrl2]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.cz_func, [target, ctrl1]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.t_func, [ctrl1]) - self.builder.call(self.h_func, [target]) - self.builder.call(self.cz_func, [ctrl2, target]) - self.builder.call(self.h_func, [target]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.cz_func, [ctrl2, ctrl1]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.t_func, [target]) - self.builder.call(self.tadj_func, [ctrl1]) - self.builder.call(self.h_func, [target]) - self.builder.call(self.cz_func, [ctrl2, target]) - self.builder.call(self.h_func, [target]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.cz_func, [target, ctrl1]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.tadj_func, [target]) - self.builder.call(self.t_func, [ctrl1]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.cz_func, [ctrl2, ctrl1]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.h_func, [target]) - call.erase() - - def _on_qis_cx(self, call, ctrl, target): - self.builder.insert_before(call) - self.builder.call(self.h_func, [target]) - self.builder.call(self.cz_func, [ctrl, target]) - self.builder.call(self.h_func, [target]) - call.erase() - - def _on_qis_cy(self, call, ctrl, target): - self.builder.insert_before(call) - self.builder.call(self.sadj_func, [target]) - self.builder.call(self.h_func, [target]) - self.builder.call(self.cz_func, [ctrl, target]) - self.builder.call(self.h_func, [target]) - self.builder.call(self.s_func, [target]) - call.erase() - - def _on_qis_rxx(self, call, angle, target1, target2): - self.builder.insert_before(call) - self.builder.call(self.h_func, [target2]) - self.builder.call(self.cz_func, [target2, target1]) - self.builder.call(self.h_func, [target1]) - self.builder.call(self.rz_func, [angle, target1]) - self.builder.call(self.h_func, [target1]) - self.builder.call(self.cz_func, [target2, target1]) - self.builder.call(self.h_func, [target2]) - call.erase() - - def _on_qis_ryy(self, call, angle, target1, target2): - self.builder.insert_before(call) - self.builder.call(self.sadj_func, [target1]) - self.builder.call(self.sadj_func, [target2]) - self.builder.call(self.h_func, [target2]) - self.builder.call(self.cz_func, [target2, target1]) - self.builder.call(self.h_func, [target1]) - self.builder.call(self.rz_func, [angle, target1]) - self.builder.call(self.h_func, [target1]) - self.builder.call(self.cz_func, [target2, target1]) - self.builder.call(self.h_func, [target2]) - self.builder.call(self.s_func, [target2]) - self.builder.call(self.s_func, [target1]) - call.erase() - - def _on_qis_rzz(self, call, angle, target1, target2): - self.builder.insert_before(call) - self.builder.call(self.h_func, [target1]) - self.builder.call(self.cz_func, [target2, target1]) - self.builder.call(self.h_func, [target1]) - self.builder.call(self.rz_func, [angle, target1]) - self.builder.call(self.h_func, [target1]) - self.builder.call(self.cz_func, [target2, target1]) - self.builder.call(self.h_func, [target1]) - call.erase() - - def _on_qis_swap(self, call, target1, target2): - self.builder.insert_before(call) - self.builder.call(self.h_func, [target2]) - self.builder.call(self.cz_func, [target1, target2]) - self.builder.call(self.h_func, [target2]) - self.builder.call(self.h_func, [target1]) - self.builder.call(self.cz_func, [target2, target1]) - self.builder.call(self.h_func, [target1]) - self.builder.call(self.h_func, [target2]) - self.builder.call(self.cz_func, [target1, target2]) - self.builder.call(self.h_func, [target2]) - call.erase() - - -class DecomposeSingleRotationToRz(QirModuleVisitor): - """ - Decomposes all single qubit rotations to Rz gates. - """ - - h_func: Function - s_func: Function - sadj_func: Function - rz_func: Function - - def _on_module(self, module): - void = Type.void(module.context) - qubit_ty = PointerType(Type.void(module.context)) - self.double_ty = Type.double(module.context) - # Find or create all the needed functions. - for func in module.functions: - match func.name: - case "__quantum__qis__h__body": - self.h_func = func - case "__quantum__qis__s__body": - self.s_func = func - case "__quantum__qis__s__adj": - self.sadj_func = func - case "__quantum__qis__rz__body": - self.rz_func = func - if not hasattr(self, "h_func"): - self.h_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__h__body", - module, - ) - if not hasattr(self, "s_func"): - self.s_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__s__body", - module, - ) - if not hasattr(self, "sadj_func"): - self.sadj_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__s__adj", - module, - ) - if not hasattr(self, "rz_func"): - self.rz_func = Function( - FunctionType(void, [self.double_ty, qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__rz__body", - module, - ) - super()._on_module(module) - - def _on_qis_rx(self, call, angle, target): - self.builder.insert_before(call) - self.builder.call(self.h_func, [target]) - self.builder.call( - self.rz_func, - [angle, target], - ) - self.builder.call(self.h_func, [target]) - call.erase() - - def _on_qis_ry(self, call, angle, target): - self.builder.insert_before(call) - self.builder.call(self.sadj_func, [target]) - self.builder.call(self.h_func, [target]) - self.builder.call( - self.rz_func, - [angle, target], - ) - self.builder.call(self.h_func, [target]) - self.builder.call(self.s_func, [target]) - call.erase() - - -class DecomposeSingleQubitToRzSX(QirModuleVisitor): - """ - Decomposes all single qubit gates to Rz and Sx gates. - """ - - sx_func: Function - rz_func: Function - - def _on_module(self, module): - void = Type.void(module.context) - qubit_ty = PointerType(Type.void(module.context)) - self.double_ty = Type.double(module.context) - # Find or create all the needed functions. - for func in module.functions: - match func.name: - case "__quantum__qis__sx__body": - self.sx_func = func - case "__quantum__qis__rz__body": - self.rz_func = func - if not hasattr(self, "sx_func"): - self.sx_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__sx__body", - module, - ) - if not hasattr(self, "rz_func"): - self.rz_func = Function( - FunctionType(void, [self.double_ty, qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__rz__body", - module, - ) - super()._on_module(module) - - def _on_qis_h(self, call, target): - self.builder.insert_before(call) - self.builder.call( - self.rz_func, - [const(self.double_ty, pi / 2), target], - ) - self.builder.call(self.sx_func, [target]) - self.builder.call( - self.rz_func, - [const(self.double_ty, pi / 2), target], - ) - call.erase() - - def _on_qis_s(self, call, target): - self.builder.insert_before(call) - self.builder.call( - self.rz_func, - [const(self.double_ty, pi / 2), target], - ) - call.erase() - - def _on_qis_s_adj(self, call, target): - self.builder.insert_before(call) - self.builder.call( - self.rz_func, - [const(self.double_ty, -pi / 2), target], - ) - call.erase() - - def _on_qis_t(self, call, target): - self.builder.insert_before(call) - self.builder.call( - self.rz_func, - [const(self.double_ty, pi / 4), target], - ) - call.erase() - - def _on_qis_t_adj(self, call, target): - self.builder.insert_before(call) - self.builder.call( - self.rz_func, - [const(self.double_ty, -pi / 4), target], - ) - call.erase() - - def _on_qis_x(self, call, target): - self.builder.insert_before(call) - self.builder.call(self.sx_func, [target]) - self.builder.call(self.sx_func, [target]) - call.erase() - - def _on_qis_y(self, call, target): - self.builder.insert_before(call) - self.builder.call(self.sx_func, [target]) - self.builder.call(self.sx_func, [target]) - self.builder.call( - self.rz_func, - [const(self.double_ty, pi), target], - ) - call.erase() - - def _on_qis_z(self, call, target): - self.builder.insert_before(call) - self.builder.call( - self.rz_func, - [const(self.double_ty, pi), target], - ) - call.erase() - - -class DecomposeRzAnglesToCliffordGates(QirModuleVisitor): - """ - Ensure that the module only contains Clifford gates instead of rotation angles. - """ - - THREE_PI_OVER_2 = 3 * pi / 2 - PI_OVER_2 = pi / 2 - TWO_PI = 2 * pi - - z_func: Function - s_func: Function - sadj_func: Function - - def _on_module(self, module): - void = Type.void(module.context) - qubit_ty = PointerType(Type.void(module.context)) - self.double_ty = Type.double(module.context) - # Find or create all the needed functions. - for func in module.functions: - match func.name: - case "__quantum__qis__s__body": - self.s_func = func - case "__quantum__qis__s__adj": - self.sadj_func = func - case "__quantum__qis__z__body": - self.z_func = func - - if not hasattr(self, "s_func"): - self.s_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__s__body", - module, - ) - if not hasattr(self, "sadj_func"): - self.sadj_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__s__adj", - module, - ) - if not hasattr(self, "z_func"): - self.z_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__z__body", - module, - ) - - super()._on_module(module) - - def _on_qis_rz(self, call, angle, target): - if not isinstance(angle, FloatConstant): - raise ValueError("Angle used in RZ must be a constant") - angle = angle.value - - self.builder.insert_before(call) - - if ( - abs(angle - self.THREE_PI_OVER_2) < TOLERANCE - or abs(angle + self.PI_OVER_2) < TOLERANCE - ): - self.builder.call(self.sadj_func, [target]) - elif abs(angle - pi) < TOLERANCE or abs(angle + pi) < TOLERANCE: - self.builder.call(self.z_func, [target]) - elif ( - abs(angle - self.PI_OVER_2) < TOLERANCE - or abs(angle + self.THREE_PI_OVER_2) < TOLERANCE - ): - self.builder.call(self.s_func, [target]) - elif ( - angle < TOLERANCE - or abs(angle - self.TWO_PI) < TOLERANCE - or abs(angle + self.TWO_PI) < TOLERANCE - ): - # I, drop it - pass - else: - raise ValueError( - f"Angle {angle} used in RZ is not a Clifford compatible rotation angle" - ) - - call.erase() - - -class ReplaceResetWithMResetZ(QirModuleVisitor): - """ - Replaces all reset operations with a call to mresetz using a new, ignored result identifier. - """ - - context: Context - mresetz_func: Function - next_result_id: int - - def _on_module(self, module): - self.context = module.context - void = Type.void(self.context) - qubit_ty = PointerType(Type.void(self.context)) - result_ty = PointerType(Type.void(self.context)) - # Find or create the intrinsic mresetz function - for func in module.functions: - match func.name: - case "__quantum__qis__mresetz__body": - self.mresetz_func = func - if not hasattr(self, "mresetz_func"): - self.mresetz_func = Function( - FunctionType(void, [qubit_ty, result_ty]), - Linkage.EXTERNAL, - "__quantum__qis__mresetz__body", - module, - ) - super()._on_module(module) - - def _on_function(self, function): - self.next_result_id = required_num_results(function) or 0 - super()._on_function(function) - - def _on_qis_reset(self, call, target): - self.builder.insert_before(call) - # Create a new result identifier to ignore the measurement result - result_id = result(self.context, self.next_result_id) - self.next_result_id += 1 - self.builder.call(self.mresetz_func, [target, result_id]) - call.erase() +# Deprecation shim – delegates to qdk._device._atom._decomp +from qdk._device._atom._decomp import * diff --git a/source/pip/qsharp/_device/_atom/_validate.py b/source/pip/qsharp/_device/_atom/_validate.py index 0ebab719f8..04e88244a9 100644 --- a/source/pip/qsharp/_device/_atom/_validate.py +++ b/source/pip/qsharp/_device/_atom/_validate.py @@ -1,45 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -from pyqir import QirModuleVisitor, is_entry_point, Opcode - - -class ValidateAllowedIntrinsics(QirModuleVisitor): - """ - Ensure that the module only contains allowed intrinsics. - """ - - def _on_function(self, function): - name = function.name - if ( - not is_entry_point(function) - and not name.endswith("_record_output") - and name - not in [ - "__quantum__rt__begin_parallel", - "__quantum__rt__end_parallel", - "__quantum__qis__read_result__body", - "__quantum__rt__read_result", - "__quantum__qis__move__body", - "__quantum__qis__cz__body", - "__quantum__qis__sx__body", - "__quantum__qis__rz__body", - "__quantum__qis__mresetz__body", - ] - ): - raise ValueError(f"{name} is not a supported intrinsic") - - -class ValidateNoConditionalBranches(QirModuleVisitor): - """ - Ensure that the function(s) only use unconditional branches. - """ - - def _on_block(self, block): - if ( - block.terminator - and block.terminator.opcode == Opcode.BR - and len(block.terminator.operands) > 1 - ): - raise ValueError("programs with branching control flow are not supported") - super()._on_block(block) +# Deprecation shim – delegates to qdk._device._atom._validate +from qdk._device._atom._validate import * diff --git a/source/pip/qsharp/_fs.py b/source/pip/qsharp/_fs.py index c317007fd1..da53438a92 100644 --- a/source/pip/qsharp/_fs.py +++ b/source/pip/qsharp/_fs.py @@ -1,90 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -""" -_fs.py - -This module provides file system utility functions for working with the file -system as Python sees it. These are used as callbacks passed into native code -to allow the native code to interact with the file system in an -environment-specific way. -""" - -import os -from typing import Dict, List, Tuple - - -def read_file(path: str) -> Tuple[str, str]: - """ - Read the contents of a file. - - :param path: The path to the file. - :return: A tuple containing the path and the file contents. - :rtype: Tuple[str, str] - """ - with open(path, mode="r", encoding="utf-8-sig") as f: - return (path, f.read()) - - -def list_directory(dir_path: str) -> List[Dict[str, str]]: - """ - Lists the contents of a directory and returns a list of dictionaries, - where each dictionary represents an entry in the directory. - - :param dir_path: The path of the directory to list. - :return: A list of dictionaries representing the entries in the directory. - Each dictionary contains the following keys: - - ``"path"``: The full path of the entry. - - ``"entry_name"``: The name of the entry. - - ``"type"``: The type of the entry: ``"file"``, ``"folder"``, or ``"unknown"``. - :rtype: List[Dict[str, str]] - """ - - def map_dir(e: str) -> Dict[str, str]: - path = os.path.join(dir_path, e) - return { - "path": path, - "entry_name": e, - "type": ( - "file" - if os.path.isfile(path) - else "folder" if os.path.isdir(path) else "unknown" - ), - } - - return list(map(map_dir, os.listdir(dir_path))) - - -def resolve(base: str, path: str) -> str: - """ - Resolves a relative path with respect to a base path. - - :param base: The base path. - :param path: The relative path. - :return: The resolved path. - :rtype: str - """ - return os.path.normpath(join(base, path)) - - -def exists(path) -> bool: - """ - Check if a file or directory exists at the given path. - - :param path: The path to the file or directory. - :return: ``True`` if the file or directory exists, ``False`` otherwise. - :rtype: bool - """ - return os.path.exists(path) - - -def join(path: str, *paths) -> str: - """ - Joins one or more path components intelligently. - - :param path: The base path. - :param *paths: Additional path components to be joined. - :return: The concatenated path. - :rtype: str - """ - return os.path.join(path, *paths) +# Deprecated: use qdk._fs instead. +from qdk._fs import * # noqa: F401,F403 diff --git a/source/pip/qsharp/_http.py b/source/pip/qsharp/_http.py index 240ddcc67f..ffe23a2d3e 100644 --- a/source/pip/qsharp/_http.py +++ b/source/pip/qsharp/_http.py @@ -1,30 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -""" -_http.py - -This module provides HTTP utility functions for interacting with -GitHub repositories. -""" - - -def fetch_github(owner: str, repo: str, ref: str, path: str) -> str: - """ - Fetches the content of a file from a GitHub repository. - - :param owner: The owner of the GitHub repository. - :param repo: The name of the GitHub repository. - :param ref: The reference (branch, tag, or commit) of the repository. - :param path: The path to the file within the repository. - :return: The content of the file as a string. - :rtype: str - :raises urllib.error.HTTPError: If there is an error fetching the file from GitHub. - :raises urllib.error.URLError: If there is an error with the URL. - """ - - import urllib.request - - path_no_leading_slash = path[1:] if path.startswith("/") else path - url = f"https://raw.githubusercontent.com/{owner}/{repo}/{ref}/{path_no_leading_slash}" - return urllib.request.urlopen(url).read().decode("utf-8-sig") +# Deprecated: use qdk._http instead. +from qdk._http import * # noqa: F401,F403 diff --git a/source/pip/qsharp/_ipython.py b/source/pip/qsharp/_ipython.py index 041148e4da..3fe5e165be 100644 --- a/source/pip/qsharp/_ipython.py +++ b/source/pip/qsharp/_ipython.py @@ -1,63 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -""" -_ipython.py - -This module provides IPython magic functions for integrating Q# code -execution within Jupyter notebooks. -""" - -from time import monotonic -from IPython.display import display, clear_output -from IPython.core.magic import register_cell_magic -from ._native import QSharpError -from ._qsharp import get_interpreter, qsharp_value_to_python_value -from . import telemetry_events - - -def register_magic(): - @register_cell_magic - def qsharp(line, cell): - """Cell magic to interpret Q# code in Jupyter notebooks.""" - # This effectively pings the kernel to ensure it recognizes the cell is running and helps with - # accureate cell execution timing. - clear_output() - - def callback(output): - display(output) - # This is a workaround to ensure that the output is flushed. This avoids an issue - # where the output is not displayed until the next output is generated or the cell - # is finished executing. - display(display_id=True) - - telemetry_events.on_run_cell() - start_time = monotonic() - - try: - results = qsharp_value_to_python_value( - get_interpreter().interpret(cell, callback) - ) - - durationMs = (monotonic() - start_time) * 1000 - telemetry_events.on_run_cell_end(durationMs) - - return results - except QSharpError as e: - # pylint: disable=raise-missing-from - raise QSharpCellError(str(e)) - - -class QSharpCellError(BaseException): - """ - Error raised when a %%qsharp cell fails. - """ - - def __init__(self, traceback: str): - self.traceback = traceback.splitlines() - - def _render_traceback_(self): - # We want to specifically override the traceback so that - # the Q# error directly from the interpreter is shown - # instead of the Python error. - return self.traceback +# Deprecated: use qdk._ipython instead. +from qdk._ipython import * # noqa: F401,F403 diff --git a/source/pip/qsharp/_native.py b/source/pip/qsharp/_native.py new file mode 100644 index 0000000000..55a3073bcd --- /dev/null +++ b/source/pip/qsharp/_native.py @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# Deprecation shim – delegates to qdk._native +from qdk._native import * # type: ignore diff --git a/source/pip/qsharp/_qsharp.py b/source/pip/qsharp/_qsharp.py index 9837989b18..70255ad810 100644 --- a/source/pip/qsharp/_qsharp.py +++ b/source/pip/qsharp/_qsharp.py @@ -1,1188 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from . import telemetry_events, code -from ._native import ( # type: ignore - Interpreter, - TargetProfile, - StateDumpData, - QSharpError, - Output, - Circuit, - GlobalCallable, - Closure, - Pauli, - Result, - UdtValue, - TypeIR, - TypeKind, - PrimitiveKind, - CircuitConfig, - CircuitGenerationMethod, - NoiseConfig, -) -from typing import ( - Any, - Callable, - Dict, - Optional, - Tuple, - TypedDict, - Union, - List, - Set, - Iterable, - cast, -) -from .estimator._estimator import ( - EstimatorResult, - EstimatorParams, - LogicalCounts, -) -import json -import os -import sys -import types -import warnings -from pathlib import Path -from time import monotonic -from dataclasses import make_dataclass - - -def lower_python_obj(obj: object, visited: Optional[Set[object]] = None) -> Any: - if visited is None: - visited = set() - - if id(obj) in visited: - raise QSharpError("Cannot send circular objects from Python to Q#.") - - visited = visited.copy().add(id(obj)) - - # Base case: Primitive types - if isinstance(obj, (bool, int, float, complex, str, Pauli, Result)): - return obj - - # Recursive case: Tuple - if isinstance(obj, tuple): - return tuple(lower_python_obj(elt, visited) for elt in obj) - - # Recursive case: Dict - if isinstance(obj, dict): - return {name: lower_python_obj(val, visited) for name, val in obj.items()} - - # Base case: Callable or Closure - if hasattr(obj, "__global_callable"): - return obj.__getattribute__("__global_callable") - if isinstance(obj, (GlobalCallable, Closure)): - return obj - - # Recursive case: Class with slots - if hasattr(obj, "__slots__"): - fields = {} - for name in getattr(obj, "__slots__"): - if name == "__dict__": - for name, val in obj.__dict__.items(): - fields[name] = lower_python_obj(val, visited) - else: - val = getattr(obj, name) - fields[name] = lower_python_obj(val, visited) - return fields - - # Recursive case: Class - if hasattr(obj, "__dict__"): - fields = { - name: lower_python_obj(val, visited) for name, val in obj.__dict__.items() - } - return fields - - # Recursive case: Array - # By using `Iterable` instead of `list`, we can handle other kind of iterables - # like numpy arrays and generators. - if isinstance(obj, Iterable): - return [lower_python_obj(elt, visited) for elt in obj] - - raise TypeError(f"unsupported type: {type(obj)}") - - -def python_args_to_interpreter_args(args): - """ - Helper function to turn the `*args` argument of this module - to the format expected by the Q# interpreter. - """ - if len(args) == 0: - return None - elif len(args) == 1: - return lower_python_obj(args[0]) - else: - return lower_python_obj(args) - - -_interpreter: Union["Interpreter", None] = None -_config: Union["Config", None] = None - -# Check if we are running in a Jupyter notebook to use the IPython display function -_in_jupyter = False -try: - from IPython.display import display - - if get_ipython().__class__.__name__ == "ZMQInteractiveShell": # type: ignore - _in_jupyter = True # Jupyter notebook or qtconsole -except: - pass - - -# Reporting execution time during IPython cells requires that IPython -# gets pinged to ensure it understands the cell is active. This is done by -# simply importing the display function, which it turns out is enough to begin timing -# while avoiding any UI changes that would be visible to the user. -def ipython_helper(): - try: - if __IPYTHON__: # type: ignore - from IPython.display import display - except NameError: - pass - - -class Config: - """ - Configuration hints for the language service. - """ - - _config: Dict[str, Any] - - def __init__( - self, - target_profile: TargetProfile, - language_features: Optional[List[str]], - manifest: Optional[str], - project_root: Optional[str], - ): - if target_profile == TargetProfile.Adaptive_RI: - self._config = {"targetProfile": "adaptive_ri"} - elif target_profile == TargetProfile.Adaptive_RIF: - self._config = {"targetProfile": "adaptive_rif"} - elif target_profile == TargetProfile.Adaptive_RIFLA: - self._config = {"targetProfile": "adaptive_rifla"} - elif target_profile == TargetProfile.Base: - self._config = {"targetProfile": "base"} - elif target_profile == TargetProfile.Unrestricted: - self._config = {"targetProfile": "unrestricted"} - - if language_features is not None: - self._config["languageFeatures"] = language_features - if manifest is not None: - self._config["manifest"] = manifest - if project_root: - # For now, we only support local project roots, so use a file schema in the URI. - # In the future, we may support other schemes, such as github, if/when - # we have VS Code Web + Jupyter support. - self._config["projectRoot"] = Path(os.getcwd(), project_root).as_uri() - - def __repr__(self) -> str: - return "Q# initialized with configuration: " + str(self._config) - - # See https://ipython.readthedocs.io/en/stable/config/integrating.html#rich-display - # See https://ipython.org/ipython-doc/3/notebook/nbformat.html#display-data - # This returns a custom MIME-type representation of the Q# configuration. - # This data will be available in the cell output, but will not be displayed - # to the user, as frontends would not know how to render the custom MIME type. - # Editor services that interact with the notebook frontend - # (i.e. the language service) can read and interpret the data. - def _repr_mimebundle_( - self, include: Union[Any, None] = None, exclude: Union[Any, None] = None - ) -> Dict[str, Dict[str, Any]]: - return {"application/x.qsharp-config": self._config} - - def get_target_profile(self) -> str: - """ - Returns the target profile as a string, or "unspecified" if not set. - """ - return self._config.get("targetProfile", "unspecified") - - -class PauliNoise(Tuple[float, float, float]): - """ - The Pauli noise to use in simulation represented - as probabilities of Pauli-X, Pauli-Y, and Pauli-Z errors - """ - - def __new__(cls, x: float, y: float, z: float): - """ - Creates a new :class:`PauliNoise` instance with the given error probabilities. - - :param x: Probability of a Pauli-X (bit flip) error. Must be non-negative. - :type x: float - :param y: Probability of a Pauli-Y error. Must be non-negative. - :type y: float - :param z: Probability of a Pauli-Z (phase flip) error. Must be non-negative. - :type z: float - :return: A new :class:`PauliNoise` tuple ``(x, y, z)``. - :rtype: PauliNoise - :raises ValueError: If any probability is negative or if ``x + y + z > 1``. - """ - if x < 0 or y < 0 or z < 0: - raise ValueError("Pauli noise probabilities must be non-negative.") - if x + y + z > 1: - raise ValueError("The sum of Pauli noise probabilities must be at most 1.") - return super().__new__(cls, (x, y, z)) - - -class DepolarizingNoise(PauliNoise): - """ - The depolarizing noise to use in simulation. - """ - - def __new__(cls, p: float): - """ - Creates a new :class:`DepolarizingNoise` instance. - - The depolarizing channel applies Pauli-X, Pauli-Y, or Pauli-Z errors each with - probability ``p / 3``. - - :param p: Total depolarizing error probability. Must satisfy ``0 ≤ p ≤ 1``. - :type p: float - :return: A new :class:`DepolarizingNoise` with equal X, Y, and Z error probabilities. - :rtype: DepolarizingNoise - :raises ValueError: If ``p`` is negative or ``p > 1``. - """ - return super().__new__(cls, p / 3, p / 3, p / 3) - - -class BitFlipNoise(PauliNoise): - """ - The bit flip noise to use in simulation. - """ - - def __new__(cls, p: float): - """ - Creates a new :class:`BitFlipNoise` instance. - - The bit flip channel applies a Pauli-X error with probability ``p``. - - :param p: Probability of a bit flip (Pauli-X) error. Must satisfy ``0 ≤ p ≤ 1``. - :type p: float - :return: A new :class:`BitFlipNoise` with X error probability ``p``. - :rtype: BitFlipNoise - :raises ValueError: If ``p`` is negative or ``p > 1``. - """ - return super().__new__(cls, p, 0, 0) - - -class PhaseFlipNoise(PauliNoise): - """ - The phase flip noise to use in simulation. - """ - - def __new__(cls, p: float): - """ - Creates a new :class:`PhaseFlipNoise` instance. - - The phase flip channel applies a Pauli-Z error with probability ``p``. - - :param p: Probability of a phase flip (Pauli-Z) error. Must satisfy ``0 ≤ p ≤ 1``. - :type p: float - :return: A new :class:`PhaseFlipNoise` with Z error probability ``p``. - :rtype: PhaseFlipNoise - :raises ValueError: If ``p`` is negative or ``p > 1``. - """ - return super().__new__(cls, 0, 0, p) - - -def init( - *, - target_profile: TargetProfile = TargetProfile.Unrestricted, - target_name: Optional[str] = None, - project_root: Optional[str] = None, - language_features: Optional[List[str]] = None, - trace_circuit: Optional[bool] = None, -) -> Config: - """ - Initializes the Q# interpreter. - - :keyword target_profile: Setting the target profile allows the Q# - interpreter to generate programs that are compatible - with a specific target. See :class:`TargetProfile`. - - :keyword target_name: An optional name of the target machine to use for inferring the compatible - target_profile setting. - - :keyword project_root: An optional path to a root directory with a Q# project to include. - It must contain a qsharp.json project manifest. - - :keyword language_features: An optional list of language feature flags to enable. - These correspond to experimental or preview Q# language features. - Valid values are: - - - ``"v2-preview-syntax"``: Enables Q# v2 preview syntax. This removes support for - the scoped qubit allocation block form (``use q = Qubit() { ... }``), requiring - the statement form instead (``use q = Qubit();``). It also removes the requirement - to use the ``set`` keyword for mutable variable assignments. - - :keyword trace_circuit: Enables tracing of circuit during execution. - Passing `True` is required for the `dump_circuit` function to return a circuit. - The `circuit` function is *NOT* affected by this parameter will always generate a circuit. - :return: The Q# interpreter configuration. - :rtype: Config - """ - from ._fs import read_file, list_directory, exists, join, resolve - from ._http import fetch_github - - global _interpreter - global _config - - if isinstance(target_name, str): - target = target_name.split(".")[0].lower() - if target == "ionq" or target == "rigetti": - target_profile = TargetProfile.Base - elif target == "quantinuum": - target_profile = TargetProfile.Adaptive_RI - else: - raise QSharpError( - f'target_name "{target_name}" not recognized. Please set target_profile directly.' - ) - - manifest_contents = None - if project_root is not None: - # Normalize the project path (i.e. fix file separators and remove unnecessary '.' and '..') - project_root = resolve(".", project_root) - qsharp_json = join(project_root, "qsharp.json") - if not exists(qsharp_json): - raise QSharpError( - f"{qsharp_json} not found. qsharp.json should exist at the project root and be a valid JSON file." - ) - - try: - (_, manifest_contents) = read_file(qsharp_json) - except Exception as e: - raise QSharpError( - f"Error reading {qsharp_json}. qsharp.json should exist at the project root and be a valid JSON file." - ) from e - - # Loop through the environment module and remove any dynamically added attributes that represent - # Q# callables or structs. This is necessary to avoid conflicts with the new interpreter instance. - keys_to_remove = [] - for key, val in code.__dict__.items(): - if ( - hasattr(val, "__global_callable") - or hasattr(val, "__qsharp_class") - or isinstance(val, types.ModuleType) - ): - keys_to_remove.append(key) - for key in keys_to_remove: - code.__delattr__(key) - - # Also remove any namespace modules dynamically added to the system. - keys_to_remove = [] - for key in sys.modules: - if key.startswith("qsharp.code."): - keys_to_remove.append(key) - for key in keys_to_remove: - sys.modules.__delitem__(key) - - _interpreter = Interpreter( - target_profile, - language_features, - project_root, - read_file, - list_directory, - resolve, - fetch_github, - _make_callable, - _make_class, - trace_circuit, - ) - - _config = Config(target_profile, language_features, manifest_contents, project_root) - # Return the configuration information to provide a hint to the - # language service through the cell output. - return _config - - -def get_interpreter() -> Interpreter: - """ - Returns the Q# interpreter. - - :return: The Q# interpreter. - :rtype: Interpreter - """ - global _interpreter - if _interpreter is None: - init() - assert _interpreter is not None, "Failed to initialize the Q# interpreter." - return _interpreter - - -def get_config() -> Config: - """ - Returns the Q# interpreter configuration. - - :return: The Q# interpreter configuration. - :rtype: Config - """ - global _config - if _config is None: - init() - assert _config is not None, "Failed to initialize the Q# interpreter." - return _config - - -class StateDump: - """ - A state dump returned from the Q# interpreter. - """ - - """ - The number of allocated qubits at the time of the dump. - """ - qubit_count: int - - __inner: dict - __data: StateDumpData - - def __init__(self, data: StateDumpData): - self.__data = data - self.__inner = data.get_dict() - self.qubit_count = data.qubit_count - - def __getitem__(self, index: int) -> complex: - return self.__inner.__getitem__(index) - - def __iter__(self): - return self.__inner.__iter__() - - def __len__(self) -> int: - return len(self.__inner) - - def __repr__(self) -> str: - return self.__data.__repr__() - - def __str__(self) -> str: - return self.__data.__str__() - - def _repr_markdown_(self) -> str: - return self.__data._repr_markdown_() - - def check_eq( - self, state: Union[Dict[int, complex], List[complex]], tolerance: float = 1e-10 - ) -> bool: - """ - Checks if the state dump is equal to the given state. This is not mathematical equality, - as the check ignores global phase. - - :param state: The state to check against, provided either as a dictionary of state indices to complex amplitudes, - or as a list of real amplitudes. - :param tolerance: The tolerance for the check. Defaults to 1e-10. - :return: ``True`` if the state dump is equal to the given state within the given tolerance, ignoring global phase. - :rtype: bool - """ - phase = None - # Convert a dense list of real amplitudes to a dictionary of state indices to complex amplitudes - if isinstance(state, list): - state = {i: val for i, val in enumerate(state)} - # Filter out zero states from the state dump and the given state based on tolerance - state = {k: v for k, v in state.items() if abs(v) > tolerance} - inner_state = {k: v for k, v in self.__inner.items() if abs(v) > tolerance} - if len(state) != len(inner_state): - return False - for key in state: - if key not in inner_state: - return False - if phase is None: - # Calculate the phase based on the first state pair encountered. - # Every pair of states after this must have the same phase for the states to be equivalent. - phase = inner_state[key] / state[key] - elif abs(phase - inner_state[key] / state[key]) > tolerance: - # This pair of states does not have the same phase, - # within tolerance, so the equivalence check fails. - return False - return True - - def as_dense_state(self) -> List[complex]: - """ - Returns the state dump as a dense list of complex amplitudes. This will include zero amplitudes. - - :return: A dense list of complex amplitudes, one per computational basis state. - :rtype: List[complex] - """ - return [self.__inner.get(i, complex(0)) for i in range(2**self.qubit_count)] - - -class ShotResult(TypedDict): - """ - A single result of a shot. - """ - - events: List[Output | StateDump | str] - result: Any - messages: List[str] - matrices: List[Output] - dumps: List[StateDump] - - -def eval( - source: str, - *, - save_events: bool = False, -) -> Any: - """ - Evaluates Q# source code. - - Output is printed to console. - - :param source: The Q# source code to evaluate. - :keyword save_events: If true, all output will be saved and returned. If false, they will be printed. - :return: The value returned by the last statement in the source code, or the saved output if ``save_events`` is true. - :rtype: Any - :raises QSharpError: If there is an error evaluating the source code. - """ - ipython_helper() - - results: ShotResult = { - "events": [], - "result": None, - "messages": [], - "matrices": [], - "dumps": [], - } - - def on_save_events(output: Output) -> None: - # Append the output to the last shot's output list - if output.is_matrix(): - results["events"].append(output) - results["matrices"].append(output) - elif output.is_state_dump(): - dump_data = cast(StateDumpData, output.state_dump()) - state_dump = StateDump(dump_data) - results["events"].append(state_dump) - results["dumps"].append(state_dump) - elif output.is_message(): - stringified = str(output) - results["events"].append(stringified) - results["messages"].append(stringified) - - def callback(output: Output) -> None: - if _in_jupyter: - try: - display(output) - return - except: - # If IPython is not available, fall back to printing the output - pass - print(output, flush=True) - - telemetry_events.on_eval() - start_time = monotonic() - - output = get_interpreter().interpret( - source, on_save_events if save_events else callback - ) - results["result"] = qsharp_value_to_python_value(output) - - durationMs = (monotonic() - start_time) * 1000 - telemetry_events.on_eval_end(durationMs) - - if save_events: - return results - else: - return results["result"] - - -# Helper function that knows how to create a function that invokes a callable. This will be -# used by the underlying native code to create functions for callables on the fly that know -# how to get the currently initialized global interpreter instance. -def _make_callable(callable: GlobalCallable, namespace: List[str], callable_name: str): - module = code - # Create a name that will be used to collect the hierarchy of namespace identifiers if they exist and use that - # to register created modules with the system. - accumulated_namespace = "qsharp.code" - accumulated_namespace += "." - for name in namespace: - accumulated_namespace += name - # Use the existing entry, which should already be a module. - if hasattr(module, name): - module = module.__getattribute__(name) - if sys.modules.get(accumulated_namespace) is None: - # This is an existing entry that is not yet registered in sys.modules, so add it. - # This can happen if a callable with the same name as this namespace is already - # defined. - sys.modules[accumulated_namespace] = module - else: - # This namespace entry doesn't exist as a module yet, so create it, add it to the environment, and - # add it to sys.modules so it supports import properly. - new_module = types.ModuleType(accumulated_namespace) - module.__setattr__(name, new_module) - sys.modules[accumulated_namespace] = new_module - module = new_module - accumulated_namespace += "." - - def _callable(*args): - ipython_helper() - - def callback(output: Output) -> None: - if _in_jupyter: - try: - display(output) - return - except: - # If IPython is not available, fall back to printing the output - pass - print(output, flush=True) - - args = python_args_to_interpreter_args(args) - - output = get_interpreter().invoke(callable, args, callback) - return qsharp_value_to_python_value(output) - - # Each callable is annotated so that we know it is auto-generated and can be removed on a re-init of the interpreter. - _callable.__global_callable = callable - - # Add the callable to the module. - if module.__dict__.get(callable_name) is None: - module.__setattr__(callable_name, _callable) - else: - # Preserve any existing attributes on the attribute with the matching name, - # since this could be a collision with an existing namespace/module. - for key, val in module.__dict__.get(callable_name).__dict__.items(): - if key != "__global_callable": - _callable.__dict__[key] = val - module.__setattr__(callable_name, _callable) - - -def qsharp_value_to_python_value(obj): - # Base case: Primitive types - if isinstance(obj, (bool, int, float, complex, str, Pauli, Result)): - return obj - - # Recursive case: Tuple - if isinstance(obj, tuple): - # Special case Value::UNIT maps to None. - if not obj: - return None - return tuple(qsharp_value_to_python_value(elt) for elt in obj) - - # Recursive case: Array - if isinstance(obj, list): - return [qsharp_value_to_python_value(elt) for elt in obj] - - # Recursive case: Callable or Closure - if isinstance(obj, (GlobalCallable, Closure)): - return obj - - # Recursive case: Udt - if isinstance(obj, UdtValue): - class_name = obj.name - fields = [] - args = [] - for name, value_ir in obj.fields: - val = qsharp_value_to_python_value(value_ir) - ty = type(val) - args.append(val) - fields.append((name, ty)) - return make_dataclass(class_name, fields)(*args) - - -def make_class_rec(qsharp_type: TypeIR) -> type: - class_name = qsharp_type.unwrap_udt().name - fields = {} - for field in qsharp_type.unwrap_udt().fields: - ty = None - kind = field[1].kind() - - if kind == TypeKind.Primitive: - prim_kind = field[1].unwrap_primitive() - if prim_kind == PrimitiveKind.Bool: - ty = bool - elif prim_kind == PrimitiveKind.Int: - ty = int - elif prim_kind == PrimitiveKind.Double: - ty = float - elif prim_kind == PrimitiveKind.Complex: - ty = complex - elif prim_kind == PrimitiveKind.String: - ty = str - elif prim_kind == PrimitiveKind.Pauli: - ty = Pauli - elif prim_kind == PrimitiveKind.Result: - ty = Result - else: - raise QSharpError(f"unknown primitive {prim_kind}") - elif kind == TypeKind.Tuple: - # Special case Value::UNIT maps to None. - if not field[1].unwrap_tuple(): - ty = type(None) - else: - ty = tuple - elif kind == TypeKind.Array: - ty = list - elif kind == TypeKind.Udt: - ty = make_class_rec(field[1]) - else: - raise QSharpError(f"unknown type {kind}") - fields[field[0]] = ty - - return make_dataclass( - class_name, - fields, - ) - - -def _make_class(qsharp_type: TypeIR, namespace: List[str], class_name: str): - """ - Helper function to create a python class given a description of it. This will be - used by the underlying native code to create classes on the fly corresponding to - the currently initialized interpreter instance. - """ - - module = code - # Create a name that will be used to collect the hierarchy of namespace identifiers if they exist and use that - # to register created modules with the system. - accumulated_namespace = "qsharp.code" - accumulated_namespace += "." - for name in namespace: - accumulated_namespace += name - # Use the existing entry, which should already be a module. - if hasattr(module, name): - module = module.__getattribute__(name) - else: - # This namespace entry doesn't exist as a module yet, so create it, add it to the environment, and - # add it to sys.modules so it supports import properly. - new_module = types.ModuleType(accumulated_namespace) - module.__setattr__(name, new_module) - sys.modules[accumulated_namespace] = new_module - module = new_module - accumulated_namespace += "." - - QSharpClass = make_class_rec(qsharp_type) - - # Each class is annotated so that we know it is auto-generated and can be removed on a re-init of the interpreter. - QSharpClass.__qsharp_class = True - - # Add the class to the module. - module.__setattr__(class_name, QSharpClass) - - -def run( - entry_expr: Union[str, Callable, GlobalCallable, Closure], - shots: int, - *args, - on_result: Optional[Callable[[ShotResult], None]] = None, - save_events: bool = False, - noise: Optional[ - Union[ - Tuple[float, float, float], - PauliNoise, - BitFlipNoise, - PhaseFlipNoise, - DepolarizingNoise, - NoiseConfig, - ] - ] = None, - qubit_loss: Optional[float] = None, - seed: Optional[int] = None, -) -> List[Any]: - """ - Runs the given Q# expression for the given number of shots. - Each shot uses an independent instance of the simulator. - - :param entry_expr: The entry expression. Alternatively, a callable can be provided, - which must be a Q# callable. - :param shots: The number of shots to run. - :param *args: The arguments to pass to the callable, if one is provided. - :param on_result: A callback function that will be called with each result. - :param save_events: If true, the output of each shot will be saved. If false, they will be printed. - :param noise: The noise to use in simulation. - :param qubit_loss: The probability of qubit loss in simulation. - :param seed: The seed to use for the random number generator in simulation, if any. - - :return: A list of results or runtime errors. If ``save_events`` is true, a list of ``ShotResult`` is returned. - :rtype: List[Any] - :raises QSharpError: If there is an error interpreting the input. - :raises ValueError: If the number of shots is less than 1. - """ - ipython_helper() - - if shots < 1: - raise ValueError("The number of shots must be greater than 0.") - - telemetry_events.on_run( - shots, - noise=(noise is not None and noise != (0.0, 0.0, 0.0)), - qubit_loss=(qubit_loss is not None and qubit_loss > 0.0), - ) - start_time = monotonic() - - results: List[ShotResult] = [] - - def print_output(output: Output) -> None: - if _in_jupyter: - try: - display(output) - return - except: - # If IPython is not available, fall back to printing the output - pass - print(output, flush=True) - - def on_save_events(output: Output) -> None: - # Append the output to the last shot's output list - results[-1]["events"].append(output) - if output.is_matrix(): - results[-1]["matrices"].append(output) - elif output.is_state_dump(): - dump_data = cast(StateDumpData, output.state_dump()) - results[-1]["dumps"].append(StateDump(dump_data)) - elif output.is_message(): - results[-1]["messages"].append(str(output)) - - callable = None - run_entry_expr = None - if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): - args = python_args_to_interpreter_args(args) - callable = entry_expr.__global_callable - elif isinstance(entry_expr, (GlobalCallable, Closure)): - args = python_args_to_interpreter_args(args) - callable = entry_expr - else: - assert isinstance(entry_expr, str) - run_entry_expr = entry_expr - - noise_config = None - if isinstance(noise, NoiseConfig): - noise_config = noise - noise = None - - shot_seed = seed - for shot in range(shots): - # We also don't want every shot to return the same results, so we update the seed for - # the next shot with the shot number. This keeps the behavior deterministic if a seed - # was provided. - if seed is not None: - shot_seed = shot + seed - - results.append( - {"result": None, "events": [], "messages": [], "matrices": [], "dumps": []} - ) - run_results = get_interpreter().run( - run_entry_expr, - on_save_events if save_events else print_output, - noise_config, - noise, - qubit_loss, - callable, - args, - shot_seed, - ) - run_results = qsharp_value_to_python_value(run_results) - results[-1]["result"] = run_results - if on_result: - on_result(results[-1]) - # For every shot after the first, treat the entry expression as None to trigger - # a rerun of the last executed expression without paying the cost for any additional - # compilation. - run_entry_expr = None - - durationMs = (monotonic() - start_time) * 1000 - telemetry_events.on_run_end(durationMs, shots) - - if save_events: - return results - else: - return [shot["result"] for shot in results] - - -# Class that wraps generated QIR, which can be used by -# azure-quantum as input data. -# -# This class must implement the QirRepresentable protocol -# that is defined by the azure-quantum package. -# See: https://github.com/microsoft/qdk-python/blob/fcd63c04aa871e49206703bbaa792329ffed13c4/azure-quantum/azure/quantum/target/target.py#L21 -class QirInputData: - # The name of this variable is defined - # by the protocol and must remain unchanged. - _name: str - - def __init__(self, name: str, ll_str: str): - self._name = name - self._ll_str = ll_str - - # The name of this method is defined - # by the protocol and must remain unchanged. - def _repr_qir_(self, **kwargs) -> bytes: - return self._ll_str.encode("utf-8") - - def __str__(self) -> str: - return self._ll_str - - -def compile( - entry_expr: Union[str, Callable, GlobalCallable, Closure], *args -) -> QirInputData: - """ - Compiles the Q# source code into a program that can be submitted to a target. - Either an entry expression or a callable with arguments must be provided. - - :param entry_expr: The Q# expression that will be used as the entrypoint - for the program. Alternatively, a callable can be provided, which must - be a Q# callable. - :param *args: The arguments to pass to the callable, if one is provided. - - :return: The compiled program. Use ``str()`` to get the QIR string. - :rtype: QirInputData - - Example: - - .. code-block:: python - program = qsharp.compile("...") - with open('myfile.ll', 'w') as file: - file.write(str(program)) - """ - ipython_helper() - start = monotonic() - interpreter = get_interpreter() - target_profile = get_config().get_target_profile() - telemetry_events.on_compile(target_profile) - if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): - args = python_args_to_interpreter_args(args) - ll_str = interpreter.qir(callable=entry_expr.__global_callable, args=args) - elif isinstance(entry_expr, (GlobalCallable, Closure)): - args = python_args_to_interpreter_args(args) - ll_str = interpreter.qir(callable=entry_expr, args=args) - else: - assert isinstance(entry_expr, str) - ll_str = interpreter.qir(entry_expr=entry_expr) - res = QirInputData("main", ll_str) - durationMs = (monotonic() - start) * 1000 - telemetry_events.on_compile_end(durationMs, target_profile) - return res - - -def circuit( - entry_expr: Optional[Union[str, Callable, GlobalCallable, Closure]] = None, - *args, - operation: Optional[str] = None, - generation_method: Optional[CircuitGenerationMethod] = None, - max_operations: Optional[int] = None, - source_locations: bool = False, - group_by_scope: bool = True, - prune_classical_qubits: bool = False, -) -> Circuit: - """ - Synthesizes a circuit for a Q# program. Either an entry - expression or an operation must be provided. - - :param entry_expr: An entry expression. Alternatively, a callable can be provided, - which must be a Q# callable. - :type entry_expr: str or Callable - - :param *args: The arguments to pass to the callable, if one is provided. - - :keyword operation: The operation to synthesize. This can be a name of - an operation or a lambda expression. The operation must take only - qubits or arrays of qubits as parameters. - :kwtype operation: str - - :keyword generation_method: The method to use for circuit generation. - :attr:`~qsharp.CircuitGenerationMethod.ClassicalEval` evaluates classical - control flow at circuit generation time. - :attr:`~qsharp.CircuitGenerationMethod.Simulate` runs a full simulation to - trace the circuit. - :attr:`~qsharp.CircuitGenerationMethod.Static` uses partial evaluation and - requires a non-``Unrestricted`` target profile. Defaults to ``None`` which - auto-selects the generation method. - :kwtype generation_method: :class:`~qsharp.CircuitGenerationMethod` - - :keyword max_operations: The maximum number of operations to include in the circuit. - Defaults to ``None`` which means no limit. - :kwtype max_operations: int - - :keyword source_locations: If ``True``, annotates each gate with its source location. - :kwtype source_locations: bool - - :keyword group_by_scope: If ``True``, groups operations by their containing scope, such as function declarations or loop blocks. - :kwtype group_by_scope: bool - - :keyword prune_classical_qubits: If ``True``, removes qubits that are never used in a quantum - gate (e.g. qubits only used as classical controls). - :kwtype prune_classical_qubits: bool - - :return: The synthesized circuit. - :rtype: :class:`~qsharp._native.Circuit` - :raises QSharpError: If there is an error synthesizing the circuit. - """ - ipython_helper() - start = monotonic() - telemetry_events.on_circuit() - config = CircuitConfig( - max_operations=max_operations, - generation_method=generation_method, - source_locations=source_locations, - group_by_scope=group_by_scope, - prune_classical_qubits=prune_classical_qubits, - ) - - if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): - args = python_args_to_interpreter_args(args) - res = get_interpreter().circuit( - config=config, callable=entry_expr.__global_callable, args=args - ) - elif isinstance(entry_expr, (GlobalCallable, Closure)): - args = python_args_to_interpreter_args(args) - res = get_interpreter().circuit(config=config, callable=entry_expr, args=args) - else: - assert entry_expr is None or isinstance(entry_expr, str) - res = get_interpreter().circuit(config, entry_expr, operation=operation) - - durationMs = (monotonic() - start) * 1000 - telemetry_events.on_circuit_end(durationMs) - - return res - - -def estimate( - entry_expr: Union[str, Callable, GlobalCallable, Closure], - params: Optional[Union[Dict[str, Any], List, EstimatorParams]] = None, - *args, -) -> EstimatorResult: - """ - Estimates resources for Q# source code. - Either an entry expression or a callable with arguments must be provided. - - :param entry_expr: The entry expression. Alternatively, a callable can be provided, - which must be a Q# callable. - :param params: The parameters to configure physical estimation. - - :return: The estimated resources. - :rtype: EstimatorResult - """ - - warnings.warn( - "This version of QRE is deprecated and will be removed in a future release. Please use the new version of QRE in qdk.qre. Refer to aka.ms/qdk.QREv3 for more information.", - DeprecationWarning, - stacklevel=2, - ) - - ipython_helper() - - def _coerce_estimator_params( - params: Optional[ - Union[Dict[str, Any], List[Dict[str, Any]], EstimatorParams] - ] = None, - ) -> List[Dict[str, Any]]: - if params is None: - return [{}] - elif isinstance(params, EstimatorParams): - if params.has_items: - return cast(List[Dict[str, Any]], params.as_dict()["items"]) - else: - return [params.as_dict()] - elif isinstance(params, dict): - return [params] - return params - - params = _coerce_estimator_params(params) - param_str = json.dumps(params) - telemetry_events.on_estimate() - start = monotonic() - if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): - args = python_args_to_interpreter_args(args) - res_str = get_interpreter().estimate( - param_str, callable=entry_expr.__global_callable, args=args - ) - elif isinstance(entry_expr, (GlobalCallable, Closure)): - args = python_args_to_interpreter_args(args) - res_str = get_interpreter().estimate(param_str, callable=entry_expr, args=args) - else: - assert isinstance(entry_expr, str) - res_str = get_interpreter().estimate(param_str, entry_expr=entry_expr) - res = json.loads(res_str) - - try: - qubits = res[0]["logicalCounts"]["numQubits"] - except (KeyError, IndexError): - qubits = "unknown" - - durationMs = (monotonic() - start) * 1000 - telemetry_events.on_estimate_end(durationMs, qubits) - return EstimatorResult(res) - - -def logical_counts( - entry_expr: Union[str, Callable, GlobalCallable, Closure], - *args, -) -> LogicalCounts: - """ - Extracts logical resource counts from Q# source code. - Either an entry expression or a callable with arguments must be provided. - - :param entry_expr: The entry expression. Alternatively, a callable can be provided, - which must be a Q# callable. - - :return: Program resources in terms of logical gate counts. - :rtype: LogicalCounts - """ - - ipython_helper() - - if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): - args = python_args_to_interpreter_args(args) - res_dict = get_interpreter().logical_counts( - callable=entry_expr.__global_callable, args=args - ) - elif isinstance(entry_expr, (GlobalCallable, Closure)): - args = python_args_to_interpreter_args(args) - res_dict = get_interpreter().logical_counts(callable=entry_expr, args=args) - else: - assert isinstance(entry_expr, str) - res_dict = get_interpreter().logical_counts(entry_expr=entry_expr) - return LogicalCounts(res_dict) - - -def set_quantum_seed(seed: Optional[int]) -> None: - """ - Sets the seed for the random number generator used for quantum measurements. - This applies to all Q# code executed, compiled, or estimated. - - :param seed: The seed to use for the quantum random number generator. - If None, the seed will be generated from entropy. - """ - get_interpreter().set_quantum_seed(seed) - - -def set_classical_seed(seed: Optional[int]) -> None: - """ - Sets the seed for the random number generator used for standard - library classical random number operations. - This applies to all Q# code executed, compiled, or estimated. - - :param seed: The seed to use for the classical random number generator. - If None, the seed will be generated from entropy. - """ - get_interpreter().set_classical_seed(seed) - - -def dump_machine() -> StateDump: - """ - Returns the sparse state vector of the simulator as a StateDump object. - - :return: The state of the simulator. - :rtype: StateDump - """ - ipython_helper() - return StateDump(get_interpreter().dump_machine()) - - -def dump_circuit() -> Circuit: - """ - Dumps a circuit showing the current state of the simulator. - - This circuit will contain the gates that have been applied - in the simulator up to the current point. - - Requires the interpreter to be initialized with `trace_circuit=True`. - - :return: The current circuit trace. - :rtype: Circuit - :raises QSharpError: If the interpreter was not initialized with ``trace_circuit=True``. - """ - ipython_helper() - return get_interpreter().dump_circuit() +# Deprecated: use qdk._types and qdk._interpreter instead. +from qdk._types import * # noqa: F401,F403 +from qdk._interpreter import * # noqa: F401,F403 diff --git a/source/pip/qsharp/_simulation.py b/source/pip/qsharp/_simulation.py index b20a2ef54b..48ce6e9a41 100644 --- a/source/pip/qsharp/_simulation.py +++ b/source/pip/qsharp/_simulation.py @@ -1,741 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from pathlib import Path -import random -from typing import Callable, Literal, List, Optional, Tuple, TypeAlias, Union -import pyqir -from ._native import ( - QirInstructionId, - QirInstruction, - run_clifford, - run_clifford_adaptive, - run_parallel_shots, - run_adaptive_parallel_shots, - run_cpu_adaptive, - run_cpu_full_state, - NoiseConfig, - GpuContext, - try_create_gpu_adapter, -) -from pyqir import ( - Function, - FunctionType, - PointerType, - Type, - Linkage, -) -from ._qsharp import QirInputData, Result -from typing import TYPE_CHECKING -from ._adaptive_pass import ( - AdaptiveProfilePass, - AdaptiveProgram, - Bytecode, - OP_RECORD_OUTPUT, -) - -if TYPE_CHECKING: # This is in the pyi file only - from ._native import GpuShotResults - - -class AggregateGatesPass(pyqir.QirModuleVisitor): - def __init__(self): - super().__init__() - self.gates: List[QirInstruction | Tuple] = [] - self.required_num_qubits = None - self.required_num_results = None - - def _get_value_as_string(self, value: pyqir.Value) -> str: - value = pyqir.extract_byte_string(value) - if value is None: - return "" - value = value.decode("utf-8") - return value - - def run(self, mod: pyqir.Module) -> Tuple[List[QirInstruction | Tuple], int, int]: - errors = mod.verify() - if errors is not None: - raise ValueError(f"Module verification failed: {errors}") - - # verify that the module is base profile - func = next(filter(pyqir.is_entry_point, mod.functions)) - self.required_num_qubits = pyqir.required_num_qubits(func) - self.required_num_results = pyqir.required_num_results(func) - - super().run(mod) - return (self.gates, self.required_num_qubits, self.required_num_results) - - def _on_block(self, block): - if ( - block.terminator - and block.terminator.opcode == pyqir.Opcode.BR - and len(block.terminator.operands) > 1 - ): - raise ValueError( - "simulation of programs with branching control flow is not supported" - ) - super()._on_block(block) - - def _on_call_instr(self, call: pyqir.Call) -> None: - callee_name = call.callee.name - if callee_name == "__quantum__qis__ccx__body": - self.gates.append( - ( - QirInstructionId.CCX, - pyqir.ptr_id(call.args[0]), - pyqir.ptr_id(call.args[1]), - pyqir.ptr_id(call.args[2]), - ) - ) - elif callee_name == "__quantum__qis__cx__body": - self.gates.append( - ( - QirInstructionId.CX, - pyqir.ptr_id(call.args[0]), - pyqir.ptr_id(call.args[1]), - ) - ) - elif callee_name == "__quantum__qis__cy__body": - self.gates.append( - ( - QirInstructionId.CY, - pyqir.ptr_id(call.args[0]), - pyqir.ptr_id(call.args[1]), - ) - ) - elif callee_name == "__quantum__qis__cz__body": - self.gates.append( - ( - QirInstructionId.CZ, - pyqir.ptr_id(call.args[0]), - pyqir.ptr_id(call.args[1]), - ) - ) - elif callee_name == "__quantum__qis__swap__body": - self.gates.append( - ( - QirInstructionId.SWAP, - pyqir.ptr_id(call.args[0]), - pyqir.ptr_id(call.args[1]), - ) - ) - elif callee_name == "__quantum__qis__rx__body": - self.gates.append( - ( - QirInstructionId.RX, - call.args[0].value, - pyqir.ptr_id(call.args[1]), - ) - ) - elif callee_name == "__quantum__qis__rxx__body": - self.gates.append( - ( - QirInstructionId.RXX, - call.args[0].value, - pyqir.ptr_id(call.args[1]), - pyqir.ptr_id(call.args[2]), - ) - ) - elif callee_name == "__quantum__qis__ry__body": - self.gates.append( - ( - QirInstructionId.RY, - call.args[0].value, - pyqir.ptr_id(call.args[1]), - ) - ) - elif callee_name == "__quantum__qis__ryy__body": - self.gates.append( - ( - QirInstructionId.RYY, - call.args[0].value, - pyqir.ptr_id(call.args[1]), - pyqir.ptr_id(call.args[2]), - ) - ) - elif callee_name == "__quantum__qis__rz__body": - self.gates.append( - ( - QirInstructionId.RZ, - call.args[0].value, - pyqir.ptr_id(call.args[1]), - ) - ) - elif callee_name == "__quantum__qis__rzz__body": - self.gates.append( - ( - QirInstructionId.RZZ, - call.args[0].value, - pyqir.ptr_id(call.args[1]), - pyqir.ptr_id(call.args[2]), - ) - ) - elif callee_name == "__quantum__qis__h__body": - self.gates.append((QirInstructionId.H, pyqir.ptr_id(call.args[0]))) - elif callee_name == "__quantum__qis__s__body": - self.gates.append((QirInstructionId.S, pyqir.ptr_id(call.args[0]))) - elif callee_name == "__quantum__qis__s__adj": - self.gates.append((QirInstructionId.SAdj, pyqir.ptr_id(call.args[0]))) - elif callee_name == "__quantum__qis__sx__body": - self.gates.append((QirInstructionId.SX, pyqir.ptr_id(call.args[0]))) - elif callee_name == "__quantum__qis__t__body": - self.gates.append((QirInstructionId.T, pyqir.ptr_id(call.args[0]))) - elif callee_name == "__quantum__qis__t__adj": - self.gates.append((QirInstructionId.TAdj, pyqir.ptr_id(call.args[0]))) - elif callee_name == "__quantum__qis__x__body": - self.gates.append((QirInstructionId.X, pyqir.ptr_id(call.args[0]))) - elif callee_name == "__quantum__qis__y__body": - self.gates.append((QirInstructionId.Y, pyqir.ptr_id(call.args[0]))) - elif callee_name == "__quantum__qis__z__body": - self.gates.append((QirInstructionId.Z, pyqir.ptr_id(call.args[0]))) - elif callee_name == "__quantum__qis__m__body": - self.gates.append( - ( - QirInstructionId.M, - pyqir.ptr_id(call.args[0]), - pyqir.ptr_id(call.args[1]), - ) - ) - elif callee_name == "__quantum__qis__mz__body": - self.gates.append( - ( - QirInstructionId.MZ, - pyqir.ptr_id(call.args[0]), - pyqir.ptr_id(call.args[1]), - ) - ) - elif callee_name == "__quantum__qis__mresetz__body": - self.gates.append( - ( - QirInstructionId.MResetZ, - pyqir.ptr_id(call.args[0]), - pyqir.ptr_id(call.args[1]), - ) - ) - elif callee_name == "__quantum__qis__reset__body": - self.gates.append((QirInstructionId.RESET, pyqir.ptr_id(call.args[0]))) - elif callee_name == "__quantum__qis__move__body": - self.gates.append( - ( - QirInstructionId.Move, - pyqir.ptr_id(call.args[0]), - ) - ) - elif callee_name == "__quantum__rt__result_record_output": - tag = self._get_value_as_string(call.args[1]) - self.gates.append( - ( - QirInstructionId.ResultRecordOutput, - str(pyqir.ptr_id(call.args[0])), - tag, - ) - ) - elif callee_name == "__quantum__rt__tuple_record_output": - tag = self._get_value_as_string(call.args[1]) - self.gates.append( - (QirInstructionId.TupleRecordOutput, str(call.args[0].value), tag) - ) - elif callee_name == "__quantum__rt__array_record_output": - tag = self._get_value_as_string(call.args[1]) - self.gates.append( - (QirInstructionId.ArrayRecordOutput, str(call.args[0].value), tag) - ) - elif ( - callee_name == "__quantum__rt__initialize" - or callee_name == "__quantum__rt__begin_parallel" - or callee_name == "__quantum__rt__end_parallel" - or callee_name == "__quantum__qis__barrier__body" - # We only hit this during noiseless simulations - or "qdk_noise" in call.callee.attributes.func - ): - pass - else: - raise ValueError(f"Unsupported call instruction: {callee_name}") - - -class CorrelatedNoisePass(AggregateGatesPass): - """ - This pass replaces the QIR intrinsics that are in the provided NoiseConfig - by correlated noise instructions that the simulator understands. - """ - - def __init__(self, noise_config: NoiseConfig): - super().__init__() - self.noise_intrinsics_table = noise_config.intrinsics - - def _on_call_instr(self, call: pyqir.Call) -> None: - callee_name = call.callee.name - if callee_name in self.noise_intrinsics_table: - self.gates.append( - ( - QirInstructionId.CorrelatedNoise, - self.noise_intrinsics_table.get_intrinsic_id(callee_name), - [pyqir.ptr_id(arg) for arg in call.args], - ) - ) - elif "qdk_noise" in call.callee.attributes.func: - # If we are running a noisy simulation, we treat - # missing noise intrinsics as an error. - raise ValueError(f"Missing noise intrinsic: {callee_name}") - else: - super()._on_call_instr(call) - - -class GpuCorrelatedNoisePass(AggregateGatesPass): - """ - A special case of the CorrelatedNoisePass that uses data loaded - directly from rust instead of a NoiseConfig object to detect the - correlated noise intrinsics. - """ - - def __init__(self, noise_table: List[Tuple[int, str, int]]): - super().__init__() - self.noise_table = dict() - for table_id, name, _count in noise_table: - self.noise_table[name] = table_id - - def _on_call_instr(self, call: pyqir.Call) -> None: - callee_name = call.callee.name - if callee_name in self.noise_table: - self.gates.append( - ( - QirInstructionId.CorrelatedNoise, - int(self.noise_table[callee_name]), # Noise table ID - [pyqir.ptr_id(qubit) for qubit in call.args], # qubit args - ) - ) - elif "qdk_noise" in call.callee.attributes.func: - # If we are running a noisy simulation, we treat - # missing noise intrinsics as an error. - raise ValueError(f"Missing noise intrinsic: {callee_name}") - else: - super()._on_call_instr(call) - - -class OutputRecordingPass(pyqir.QirModuleVisitor): - _output_str = "" - _closers = [] - _counters = [] - - def process_output(self, bitstring: str): - return eval( - self._output_str, - { - "o": [ - Result.Zero if x == "0" else Result.One if x == "1" else Result.Loss - for x in bitstring - ] - }, - ) - - def _on_function(self, function): - if pyqir.is_entry_point(function): - super()._on_function(function) - while len(self._closers) > 0: - self._output_str += self._closers.pop() - self._counters.pop() - - def _on_rt_result_record_output(self, call, result, target): - self._output_str += f"o[{pyqir.ptr_id(result)}]" - while len(self._counters) > 0: - self._output_str += "," - self._counters[-1] -= 1 - if self._counters[-1] == 0: - self._output_str += self._closers[-1] - self._closers.pop() - self._counters.pop() - else: - break - - def _on_rt_array_record_output(self, call, value, target): - self._output_str += "[" - self._closers.append("]") - # if len(self._counters) > 0: - # self._counters[-1] -= 1 - self._counters.append(value.value) - - def _on_rt_tuple_record_output(self, call, value, target): - self._output_str += "(" - self._closers.append(")") - # if len(self._counters) > 0: - # self._counters[-1] -= 1 - self._counters.append(value.value) - - -class DecomposeCcxPass(pyqir.QirModuleVisitor): - - h_func: Function - t_func: Function - tadj_func: Function - cz_func: Function - - def __init__(self): - super().__init__() - - def _on_module(self, module): - void = Type.void(module.context) - qubit_ty = PointerType(Type.void(module.context)) - - # Find or create all the needed functions. - for func in module.functions: - match func.name: - case "__quantum__qis__h__body": - self.h_func = func - case "__quantum__qis__t__body": - self.t_func = func - case "__quantum__qis__t__adj": - self.tadj_func = func - case "__quantum__qis__cz__body": - self.cz_func = func - if not hasattr(self, "h_func"): - self.h_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__h__body", - module, - ) - if not hasattr(self, "t_func"): - self.t_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__t__body", - module, - ) - if not hasattr(self, "tadj_func"): - self.tadj_func = Function( - FunctionType(void, [qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__t__adj", - module, - ) - if not hasattr(self, "cz_func"): - self.cz_func = Function( - FunctionType(void, [qubit_ty, qubit_ty]), - Linkage.EXTERNAL, - "__quantum__qis__cz__body", - module, - ) - super()._on_module(module) - - def _on_qis_ccx(self, call, ctrl1, ctrl2, target): - self.builder.insert_before(call) - self.builder.call(self.h_func, [target]) - self.builder.call(self.tadj_func, [ctrl1]) - self.builder.call(self.tadj_func, [ctrl2]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.cz_func, [target, ctrl1]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.t_func, [ctrl1]) - self.builder.call(self.h_func, [target]) - self.builder.call(self.cz_func, [ctrl2, target]) - self.builder.call(self.h_func, [target]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.cz_func, [ctrl2, ctrl1]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.t_func, [target]) - self.builder.call(self.tadj_func, [ctrl1]) - self.builder.call(self.h_func, [target]) - self.builder.call(self.cz_func, [ctrl2, target]) - self.builder.call(self.h_func, [target]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.cz_func, [target, ctrl1]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.tadj_func, [target]) - self.builder.call(self.t_func, [ctrl1]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.cz_func, [ctrl2, ctrl1]) - self.builder.call(self.h_func, [ctrl1]) - self.builder.call(self.h_func, [target]) - call.erase() - - -Simulator: TypeAlias = Callable[ - [List[QirInstruction], int, int, int, NoiseConfig, int], str -] - - -def preprocess_simulation_input( - input: Union[QirInputData, str, bytes], - shots: Optional[int] = 1, - noise: Optional[NoiseConfig] = None, - seed: Optional[int] = None, -) -> tuple[pyqir.Module, int, Optional[NoiseConfig], int]: - if shots is None: - shots = 1 - # If no seed specified, generate a random u32 to use - if seed is None: - seed = random.randint(0, 2**32 - 1) - if isinstance(noise, tuple): - raise ValueError( - "Specifying Pauli noise via a tuple is not supported. Use a NoiseConfig instead." - ) - - context = pyqir.Context() - if isinstance(input, QirInputData): - mod = pyqir.Module.from_ir(context, str(input)) - elif isinstance(input, str): - mod = pyqir.Module.from_ir(context, input) - else: - mod = pyqir.Module.from_bitcode(context, input) - - return (mod, shots, noise, seed) - - -def is_adaptive(mod: pyqir.Module) -> bool: - """Check if the QIR module uses the Adaptive Profile.""" - entry = next(filter(pyqir.is_entry_point, mod.functions), None) - if entry is None: - return False - func_attrs = entry.attributes.func - if "qir_profiles" not in func_attrs: - return False - return func_attrs["qir_profiles"].string_value == "adaptive_profile" - - -def str_to_result(result: str): - match result: - case "0": - return Result.Zero - case "1": - return Result.One - case "L": - return Result.Loss - case _: - raise ValueError(f"Invalid result {result}") - - -def run_base( - rust_run_base_fn: Callable, - mod: pyqir.Module, - shots: int, - noise: Optional[NoiseConfig], - seed: int, -): - """ - Runs a base profile program given a rust simulator. Adds output recording logic. - """ - if noise is None: - (gates, num_qubits, num_results) = AggregateGatesPass().run(mod) - else: - (gates, num_qubits, num_results) = CorrelatedNoisePass(noise).run(mod) - recorder = OutputRecordingPass() - recorder.run(mod) - return list( - map( - recorder.process_output, - rust_run_base_fn(gates, num_qubits, num_results, shots, noise, seed), - ) - ) - - -def run_adaptive( - rust_run_adaptive_fn: Callable, - mod: pyqir.Module, - program: AdaptiveProgram, - shots: int, - noise: Optional[NoiseConfig], - seed: int, -): - """ - Runs an adaptive profile program given a rust simulator. Adds output recording logic. - """ - results = rust_run_adaptive_fn(program.as_dict(), shots, noise, seed) - recorder = OutputRecordingPass() - recorder.run(mod) - return list(map(recorder.process_output, results)) - - -def run_qir_clifford( - input: Union[QirInputData, str, bytes], - shots: Optional[int] = 1, - noise: Optional[NoiseConfig] = None, - seed: Optional[int] = None, -) -> List: - (mod, shots, noise, seed) = preprocess_simulation_input(input, shots, noise, seed) - if is_adaptive(mod): - program = AdaptiveProfilePass(Bytecode.Bit64).run(mod, noise) - return run_adaptive(run_clifford_adaptive, mod, program, shots, noise, seed) - else: - return run_base(run_clifford, mod, shots, noise, seed) - - -def run_qir_cpu( - input: Union[QirInputData, str, bytes], - shots: Optional[int] = 1, - noise: Optional[NoiseConfig] = None, - seed: Optional[int] = None, -) -> List: - (mod, shots, noise, seed) = preprocess_simulation_input(input, shots, noise, seed) - DecomposeCcxPass().run(mod) - if is_adaptive(mod): - program = AdaptiveProfilePass(Bytecode.Bit64).run(mod, noise) - return run_adaptive(run_cpu_adaptive, mod, program, shots, noise, seed) - else: - return run_base(run_cpu_full_state, mod, shots, noise, seed) - - -def run_qir_gpu( - input: Union[QirInputData, str, bytes], - shots: Optional[int] = 1, - noise: Optional[NoiseConfig] = None, - seed: Optional[int] = None, -) -> List: - (mod, shots, noise, seed) = preprocess_simulation_input(input, shots, noise, seed) - # Ccx is not support in the GPU simulator, decompose it - DecomposeCcxPass().run(mod) - if is_adaptive(mod): - program = AdaptiveProfilePass(Bytecode.Bit32).run(mod, noise) - return run_adaptive( - run_adaptive_parallel_shots, mod, program, shots, noise, seed - ) - else: - return run_base(run_parallel_shots, mod, shots, noise, seed) - - -def prepare_qir_with_correlated_noise( - input: Union[QirInputData, str, bytes], - noise_tables: List[Tuple[int, str, int]], -) -> Tuple[List[QirInstruction], int, int]: - # Turn the input into a QIR module - (mod, _, _, _) = preprocess_simulation_input(input, None, None, None) - - # Ccx is not support in the GPU simulator, decompose it - DecomposeCcxPass().run(mod) - - # Extract the gates including correlated noise instructions - (gates, required_num_qubits, required_num_results) = GpuCorrelatedNoisePass( - noise_tables - ).run(mod) - - return (gates, required_num_qubits, required_num_results) - - -class GpuSimulator: - """ - Represents a GPU-based QIR simulator. This is a 'full state' simulator that can simulate - quantum programs, including non-Clifford gates, up to a limit of 27 qubits. - """ - - def __init__(self): - self.gpu_context = GpuContext() - self._is_adaptive = False - self._recorder = None - self.tables = None - - def load_noise_tables( - self, - noise_dir: str, - ): - """ - Loads noise tables from the specified directory path. For each .csv file found in the directory, - the noise table is loaded and associated with a unique identifier. The name of the file (without the .csv extension) - is used as the label for the noise table, which should match the QIR instruction that will apply noise using this table. - - If testing various noise models, you may load new noise models at any time by calling this method again - with a different directory path. Previously loaded noise tables will be replaced. The program currently loaded - into the simulator (if any) will remain loaded, but any subsequent calls to `run_shots` will use the newly loaded noise tables. - - Each line of the table should be of the format: "IXYZ,1.345e-4" where IXYZ is a string of Pauli operators - representing the error on each qubit (Z applying to the first qubit argument, Y to the second, etc.), and the second value - is the corresponding error probability for that specific Pauli string. - - Blank lines, lines starting with #, or lines that start with the string "pauli" (i.e., a column header) are ignored. - """ - self.tables = self.gpu_context.load_noise_tables(noise_dir) - - def set_program(self, input: Union[QirInputData, str, bytes]): - """ - Load the QIR program into the GPU simulator, preparing it for execution. You may load and run - multiple programs sequentially by calling this method multiple times before calling `run_shots` - without needing to create a new simulator instance or reloading noise tables. - """ - # Parse the QIR module to detect profile - (mod, _, _, _) = preprocess_simulation_input(input, None, None, None) - if is_adaptive(mod): - self._is_adaptive = True - # Build noise_intrinsics dict from loaded noise tables (if any) - noise_intrinsics = None - if self.tables is not None: - noise_intrinsics = {name: table_id for table_id, name, _ in self.tables} - program = AdaptiveProfilePass(Bytecode.Bit32).run( - mod, noise_intrinsics=noise_intrinsics - ) - self.gpu_context.set_adaptive_program(program.as_dict()) - # This is used later for output recording - self._recorder = OutputRecordingPass() - self._recorder.run(mod) - else: - self._is_adaptive = False - (self.gates, self.required_num_qubits, self.required_num_results) = ( - prepare_qir_with_correlated_noise( - input, self.tables if not self.tables is None else [] - ) - ) - self.gpu_context.set_program( - self.gates, self.required_num_qubits, self.required_num_results - ) - - def run_shots(self, shots: int, seed: Optional[int] = None) -> "GpuShotResults": - """ - Run the loaded QIR program for the specified number of shots, using an optional seed for reproducibility. - If noise is to be applied, ensure that noise has been loaded prior to running shots. - """ - seed = seed if seed is not None else random.randint(0, 2**32 - 1) - if self._is_adaptive: - results = self.gpu_context.run_adaptive_shots(shots, seed=seed) - for i, (shot_ret_code, shot_result) in enumerate( - zip(results["shot_result_codes"], results["shot_results"]) - ): - if shot_ret_code == 0: - # If the ret_code was zero, we do an output recording pass - # on the output. - results["shot_results"][i] = self._recorder.process_output( - shot_result - ) - else: - # If the shot finished with a ret_code other than zero, - # we set the result to `None`. - results["shot_results"][i] = None - return results - return self.gpu_context.run_shots(shots, seed=seed) - - -def run_qir( - input: Union[QirInputData, str, bytes], - shots: Optional[int] = 1, - noise: Optional[NoiseConfig] = None, - seed: Optional[int] = None, - type: Optional[Literal["clifford", "cpu", "gpu"]] = None, -) -> List: - """ - Simulate the given QIR source. - - :param input: The QIR source to simulate. - :param type: The type of simulator to use. - Use ``"clifford"`` if your QIR only contains Clifford gates and measurements. - Use ``"gpu"`` if you have a GPU available in your system. - Use ``"cpu"`` as a fallback option if you don't have a GPU in your system. - If ``None`` (default), the GPU simulator will be tried first, falling back to - CPU if a suitable GPU device could not be located. - :param shots: The number of shots to run. - :param noise: A noise model to use in the simulation. - :param seed: A seed for reproducibility. - :return: A list of measurement results, in the order they happened during the simulation. - :rtype: List - """ - if type is None: - try: - try_create_gpu_adapter() - type = "gpu" - except OSError: - type = "cpu" - - match type: - case "clifford": - return run_qir_clifford(input, shots, noise, seed) - case "cpu": - return run_qir_cpu(input, shots, noise, seed) - case "gpu": - return run_qir_gpu(input, shots, noise, seed) - case _: - raise ValueError(f"Invalid simulator type: {type}") +# Deprecated: use qdk.simulation instead. +from qdk.simulation._simulation import * # noqa: F401,F403 diff --git a/source/pip/qsharp/applications/__init__.py b/source/pip/qsharp/applications/__init__.py index 59e481eb93..bbae5f019b 100644 --- a/source/pip/qsharp/applications/__init__.py +++ b/source/pip/qsharp/applications/__init__.py @@ -1,2 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. + +# Deprecated: use qdk.applications instead. +from qdk.applications import * # noqa: F401,F403 diff --git a/source/pip/qsharp/applications/magnets/__init__.py b/source/pip/qsharp/applications/magnets/__init__.py index 56c536659e..2956378270 100644 --- a/source/pip/qsharp/applications/magnets/__init__.py +++ b/source/pip/qsharp/applications/magnets/__init__.py @@ -1,14 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -# flake8: noqa F403 -# pyright: ignore[reportWildcardImportFromLibrary] - -"""Magnets application module. - -Re-exports from the submodules.""" - -from .geometry import * -from .models import * -from .trotter import * -from .utilities import * +# Deprecation shim – delegates to qdk.applications.magnets +from qdk.applications.magnets import * diff --git a/source/pip/qsharp/code/__init__.py b/source/pip/qsharp/code/__init__.py index 695b54fb63..4850ce0a7e 100644 --- a/source/pip/qsharp/code/__init__.py +++ b/source/pip/qsharp/code/__init__.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -""" -Code module that receives any user-defined Q# callables as Python functions. -""" +# Deprecated: use qdk.code instead. +from qdk.code import * # noqa: F401,F403 diff --git a/source/pip/qsharp/estimator/__init__.py b/source/pip/qsharp/estimator/__init__.py index ef870f3dad..54564d8c75 100644 --- a/source/pip/qsharp/estimator/__init__.py +++ b/source/pip/qsharp/estimator/__init__.py @@ -1,36 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from ._estimator import ( - EstimatorError, - LogicalCounts, - EstimatorResult, - QubitParams, - QECScheme, - MeasurementErrorRate, - EstimatorQubitParams, - EstimatorQecScheme, - ProtocolSpecificDistillationUnitSpecification, - DistillationUnitSpecification, - ErrorBudgetPartition, - EstimatorConstraints, - EstimatorInputParamsItem, - EstimatorParams, -) - -__all__ = [ - "EstimatorError", - "LogicalCounts", - "EstimatorResult", - "QubitParams", - "QECScheme", - "MeasurementErrorRate", - "EstimatorQubitParams", - "EstimatorQecScheme", - "ProtocolSpecificDistillationUnitSpecification", - "DistillationUnitSpecification", - "ErrorBudgetPartition", - "EstimatorConstraints", - "EstimatorInputParamsItem", - "EstimatorParams", -] +# Deprecated: use qdk.estimator instead. +from qdk.estimator import * # noqa: F401,F403 diff --git a/source/pip/qsharp/interop/__init__.py b/source/pip/qsharp/interop/__init__.py index 379559deb5..2e8d11cac6 100644 --- a/source/pip/qsharp/interop/__init__.py +++ b/source/pip/qsharp/interop/__init__.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -"""Interoperability modules for the Q# ecosystem.""" +# Deprecated: use qdk.interop (qdk.qiskit / qdk.cirq) instead. diff --git a/source/pip/qsharp/interop/cirq/__init__.py b/source/pip/qsharp/interop/cirq/__init__.py index 8a484fc8ab..b814dfd25b 100644 --- a/source/pip/qsharp/interop/cirq/__init__.py +++ b/source/pip/qsharp/interop/cirq/__init__.py @@ -1,33 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -"""Cirq interoperability for the Q# ecosystem. - -This module provides a :class:`~qsharp.interop.cirq.NeutralAtomSampler` — a standard -``cirq.Sampler`` that runs Cirq circuits on the local NeutralAtomDevice -simulator. - -Usage: - - import cirq - from qsharp.interop.cirq import NeutralAtomSampler - - q0, q1 = cirq.LineQubit.range(2) - circuit = cirq.Circuit([ - cirq.H(q0), - cirq.CNOT(q0, q1), - cirq.measure(q0, q1, key="m"), - ]) - - sampler = NeutralAtomSampler(seed=42) - result = sampler.run(circuit, repetitions=1000) - print(result.histogram(key="m")) -""" - -from ._neutral_atom import NeutralAtomSampler -from ._result import NeutralAtomCirqResult - -__all__ = [ - "NeutralAtomSampler", - "NeutralAtomCirqResult", -] +# Deprecated: use qdk.cirq instead. +from qdk.cirq import * # noqa: F401,F403 diff --git a/source/pip/qsharp/interop/qiskit/__init__.py b/source/pip/qsharp/interop/qiskit/__init__.py index 480e2761b7..3718bfc99b 100644 --- a/source/pip/qsharp/interop/qiskit/__init__.py +++ b/source/pip/qsharp/interop/qiskit/__init__.py @@ -1,109 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -"""Qiskit interoperability for the Q# ecosystem. - -This module provides Qiskit backends backed by the local Q# simulator and -NeutralAtomDevice, allowing Qiskit circuits to be run locally without any -cloud connection. - -Available backends: - -- :class:`~qsharp.interop.qiskit.QSharpBackend` - Runs any Qiskit ``QuantumCircuit`` using the Q# simulator. Supports - noise-free simulation via QASM export and QIR compilation. - -- :class:`~qsharp.interop.qiskit.NeutralAtomBackend` - Runs Qiskit circuits on the local NeutralAtomDevice simulator. Decomposes - gates to the native ``{Rz, SX, CZ}`` gate set and optionally models - per-gate noise (including qubit loss). Loss shots are exposed separately - from accepted shots in the job result. - -- :class:`~qsharp.interop.qiskit.ResourceEstimatorBackend` - Estimates quantum resources (qubits, T-gates, etc.) for a Qiskit circuit - without running a full simulation. - -- :func:`~qsharp.interop.qiskit.estimate` - Convenience function that runs resource estimation on a Qiskit circuit - and returns an :class:`~qsharp.estimator.EstimatorResult` directly, without - needing to construct a backend or job manually. - -Usage: - - from qiskit import QuantumCircuit - from qsharp.interop.qiskit import NeutralAtomBackend - from qsharp._simulation import NoiseConfig - - circuit = QuantumCircuit(2, 2) - circuit.h(0) - circuit.cx(0, 1) - circuit.measure([0, 1], [0, 1]) - - noise = NoiseConfig() - noise.rz.loss = 0.05 # 5% qubit loss per Rz gate - - backend = NeutralAtomBackend() - job = backend.run(circuit, shots=1000, noise=noise, seed=42) - result = job.result() - print(result.results[0].data.counts) # accepted shots only - print(result.results[0].data.raw_counts) # includes loss shots -""" -from typing import Any, Dict, List, Optional, Union - -from ...estimator import EstimatorParams, EstimatorResult -from ..._native import OutputSemantics, ProgramType, QasmError -from .backends import ( - NeutralAtomBackend, - QSharpBackend, - ResourceEstimatorBackend, - QirTarget, -) -from .jobs import QsJob, QsSimJob, ReJob, QsJobSet -from .execution import DetaultExecutor -from qiskit import QuantumCircuit - - -def estimate( - circuit: QuantumCircuit, - params: Optional[Union[Dict[str, Any], List, EstimatorParams]] = None, - **options, -) -> EstimatorResult: - """ - Estimates resources for Qiskit QuantumCircuit. - - :param circuit: The input Qiskit QuantumCircuit object. - :param params: The parameters to configure physical estimation. - :type params: EstimatorParams or dict or list - :param **options: Additional options for the transpiler, exporter, or Qiskit passes - configuration. Defaults to backend config values. Common options: - - - ``optimization_level`` (int): Transpiler optimization level. - - ``basis_gates`` (list): Basis gates for transpilation. - - ``includes`` (list): Include paths for QASM resolution. - - ``search_path`` (str): Search path for resolving file references. - :raises QasmError: If there is an error generating or parsing QASM. - :return: The estimated resources. - :rtype: EstimatorResult - """ - from ..._qsharp import ipython_helper - - ipython_helper() - backend = ResourceEstimatorBackend() - job = backend.run(circuit, params=params, **options) - return job.result() - - -__all__ = [ - "NeutralAtomBackend", - "QSharpBackend", - "ResourceEstimatorBackend", - "QirTarget", - "QsJob", - "QsSimJob", - "ReJob", - "QsJobSet", - "estimate", - "EstimatorParams", - "EstimatorResult", - "QasmError", -] +# Deprecated: use qdk.qiskit instead. +from qdk.qiskit import * # noqa: F401,F403 diff --git a/source/pip/qsharp/noisy_simulator/__init__.py b/source/pip/qsharp/noisy_simulator/__init__.py index af73d8f073..61805a2899 100644 --- a/source/pip/qsharp/noisy_simulator/__init__.py +++ b/source/pip/qsharp/noisy_simulator/__init__.py @@ -1,7 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from ._noisy_simulator import ( +# Deprecated: use qdk.simulation instead. +from qdk.simulation import ( # noqa: F401 NoisySimulatorError, Operation, Instrument, @@ -10,13 +11,3 @@ DensityMatrix, StateVector, ) - -__all__ = [ - "NoisySimulatorError", - "Operation", - "Instrument", - "DensityMatrixSimulator", - "StateVectorSimulator", - "DensityMatrix", - "StateVector", -] diff --git a/source/pip/qsharp/openqasm/__init__.py b/source/pip/qsharp/openqasm/__init__.py index 5e9d3757a0..b188ee05a5 100644 --- a/source/pip/qsharp/openqasm/__init__.py +++ b/source/pip/qsharp/openqasm/__init__.py @@ -1,20 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from ._circuit import circuit -from ._compile import compile -from ._estimate import estimate -from ._import import import_openqasm -from ._run import run -from .._native import ProgramType, OutputSemantics, QasmError # type: ignore - -__all__ = [ - "circuit", - "compile", - "estimate", - "import_openqasm", - "run", - "ProgramType", - "OutputSemantics", - "QasmError", -] +# Deprecated: use qdk.openqasm instead. +from qdk.openqasm import * # noqa: F401,F403 diff --git a/source/pip/qsharp/qre/__init__.py b/source/pip/qsharp/qre/__init__.py index 0dbe8d4a9d..1c2ed76824 100644 --- a/source/pip/qsharp/qre/__init__.py +++ b/source/pip/qsharp/qre/__init__.py @@ -1,86 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from ._application import Application -from ._architecture import Architecture, ISAContext -from ._estimation import estimate -from ._instruction import ( - LOGICAL, - PHYSICAL, - Encoding, - ISATransform, - constraint, - InstructionSource, -) -from ._isa_enumeration import ISAQuery, ISARefNode, ISA_ROOT -from ._qre import ( - ISA, - InstructionFrontier, - Constraint, - ConstraintBound, - EstimationResult, - FactoryResult, - ISARequirements, - Block, - Trace, - block_linear_function, - constant_function, - generic_function, - linear_function, - instruction_name, - property_name, - property_name_to_key, -) -from ._results import ( - EstimationTable, - EstimationTableColumn, - EstimationTableEntry, - plot_estimates, -) -from ._trace import LatticeSurgery, PSSPC, TraceQuery, TraceTransform - -# Extend Rust Python types with additional Python-side functionality -from ._instruction import _isa_as_frame, _requirements_as_frame - -ISA.as_frame = _isa_as_frame -ISARequirements.as_frame = _requirements_as_frame - -__all__ = [ - "block_linear_function", - "constant_function", - "constraint", - "estimate", - "linear_function", - "plot_estimates", - "Application", - "Architecture", - "Block", - "Constraint", - "ConstraintBound", - "Encoding", - "EstimationResult", - "EstimationTable", - "EstimationTableColumn", - "EstimationTableEntry", - "FactoryResult", - "generic_function", - "instruction_name", - "InstructionFrontier", - "InstructionSource", - "ISA", - "ISA_ROOT", - "ISAContext", - "ISAQuery", - "ISARefNode", - "ISARequirements", - "ISATransform", - "LatticeSurgery", - "PSSPC", - "property_name", - "property_name_to_key", - "Trace", - "TraceQuery", - "TraceTransform", - "LOGICAL", - "PHYSICAL", -] +# Deprecated: use qdk.qre instead. +from qdk.qre import * # noqa: F401,F403 diff --git a/source/pip/qsharp/qre/_architecture.py b/source/pip/qsharp/qre/_architecture.py index cd8bb52e64..371fc6fdf3 100644 --- a/source/pip/qsharp/qre/_architecture.py +++ b/source/pip/qsharp/qre/_architecture.py @@ -1,244 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -from __future__ import annotations -import copy -from typing import cast, TYPE_CHECKING - -from abc import ABC, abstractmethod - -from ._qre import ( - ISA, - _ProvenanceGraph, - Instruction, - _IntFunction, - _FloatFunction, - constant_function, - property_name_to_key, -) - -if TYPE_CHECKING: - from typing import Optional - - from ._instruction import ISATransform, Encoding - - -class Architecture(ABC): - """Abstract base class for quantum hardware architectures.""" - - @abstractmethod - def provided_isa(self, ctx: ISAContext) -> ISA: - """ - Create the ISA provided by this architecture, adding instructions - directly to the context's provenance graph. - - Args: - ctx (ISAContext): The enumeration context whose provenance graph stores - the instructions. - - Returns: - ISA: The ISA backed by the context's provenance graph. - """ - ... - - def context(self) -> ISAContext: - """Create a new enumeration context for this architecture. - - Returns: - ISAContext: A new enumeration context. - """ - return ISAContext(self) - - -class ISAContext: - """ - Context passed through enumeration, holding shared state. - """ - - def __init__(self, arch: Architecture): - """Initialize the ISA context for the given architecture. - - Args: - arch (Architecture): The architecture providing the base ISA. - """ - self._provenance: _ProvenanceGraph = _ProvenanceGraph() - - # Let the architecture create instructions directly in the graph. - self._isa = arch.provided_isa(self) - - self._bindings: dict[str, ISA] = {} - self._transforms: dict[int, Architecture | ISATransform] = {0: arch} - - def _with_binding(self, name: str, isa: ISA) -> ISAContext: - """Return a new context with an additional binding (internal use).""" - ctx = copy.copy(self) - ctx._bindings = {**self._bindings, name: isa} - return ctx - - @property - def isa(self) -> ISA: - """The ISA provided by the architecture for this context.""" - return self._isa - - def add_instruction( - self, - id_or_instruction: int | Instruction, - encoding: Encoding = 0, # type: ignore - *, - arity: Optional[int] = 1, - time: int | _IntFunction = 0, - space: Optional[int] | _IntFunction = None, - length: Optional[int | _IntFunction] = None, - error_rate: float | _FloatFunction = 0.0, - transform: ISATransform | None = None, - source: list[Instruction] | None = None, - **kwargs: int, - ) -> int: - """ - Create an instruction and add it to the provenance graph. - - Can be called in two ways: - - 1. With keyword args to create a new instruction:: - - ctx.add_instruction(T, encoding=LOGICAL, time=1000, - error_rate=1e-8) - - 2. With a pre-existing ``Instruction`` object (e.g. from - ``with_id()``):: - - ctx.add_instruction(existing_instruction) - - Provenance is recorded when *transform* and/or *source* are - supplied: - - - **transform** — the ``ISATransform`` that produced the - instruction. - - **source** — input instructions consumed by the transform. - - Args: - id_or_instruction: Either an instruction ID (int) for creating - a new instruction, or an existing ``Instruction`` object. - encoding: The instruction encoding (0 = Physical, 1 = Logical). - Ignored when passing an existing ``Instruction``. - arity: The instruction arity. ``None`` for variable arity. - Ignored when passing an existing ``Instruction``. - time: Instruction time in ns (or ``_IntFunction`` for variable - arity). Ignored when passing an existing ``Instruction``. - space: Instruction space in physical qubits (or ``_IntFunction`` - for variable arity). Ignored when passing an existing - ``Instruction``. - length: Arity including ancilla qubits. Ignored when passing an - existing ``Instruction``. - error_rate: Instruction error rate (or ``_FloatFunction`` for - variable arity). Ignored when passing an existing - ``Instruction``. - transform: The ``ISATransform`` that produced the instruction. - source: List of source ``Instruction`` objects consumed by the - transform. - **kwargs: Additional properties (e.g. ``distance=9``). Ignored - when passing an existing ``Instruction``. - - Returns: - The node index in the provenance graph. - - Raises: - ValueError: If an unknown property name is provided in kwargs. - """ - if transform is None and source is None: - return self._provenance.add_instruction( - cast(int, id_or_instruction), - encoding, - arity=arity, - time=time, - space=space, - length=length, - error_rate=error_rate, - **kwargs, - ) - - if isinstance(id_or_instruction, Instruction): - instr = id_or_instruction - else: - instr = _make_instruction( - id_or_instruction, - int(encoding), - arity, - time, - space, - length, - error_rate, - kwargs, - ) - - transform_id = id(transform) if transform is not None else 0 - children = [inst.source for inst in source] if source else [] - - node_index = self._provenance.add_node(instr, transform_id, children) - - if transform is not None: - self._transforms[transform_id] = transform - - return node_index - - def make_isa(self, *node_indices: int) -> ISA: - """ - Create an ISA backed by this context's provenance graph from the - given node indices. - - Args: - *node_indices (int): Node indices in the provenance graph. - - Returns: - ISA: An ISA referencing the provenance graph. - """ - return self._provenance.make_isa(list(node_indices)) - - -def _make_instruction( - id: int, - encoding: int, - arity: int | None, - time: int | _IntFunction, - space: int | _IntFunction | None, - length: int | _IntFunction | None, - error_rate: float | _FloatFunction, - properties: dict[str, int], -) -> Instruction: - """Build an ``Instruction`` from keyword arguments.""" - if arity is not None: - instr = Instruction.fixed_arity( - id, - encoding, - arity, - cast(int, time), - cast(int | None, space), - cast(int | None, length), - cast(float, error_rate), - ) - else: - if isinstance(time, int): - time = constant_function(time) - if isinstance(space, int): - space = constant_function(space) - if isinstance(length, int): - length = constant_function(length) - if isinstance(error_rate, (int, float)): - error_rate = constant_function(float(error_rate)) - - instr = Instruction.variable_arity( - id, - encoding, - time, - cast(_IntFunction, space), - error_rate, - length, - ) - - for key, value in properties.items(): - prop_key = property_name_to_key(key) - if prop_key is None: - raise ValueError(f"Unknown property '{key}'.") - instr.set_property(prop_key, value) - - return instr +# Deprecation shim – delegates to qdk.qre._architecture +from qdk.qre._architecture import * diff --git a/source/pip/qsharp/qre/_enumeration.py b/source/pip/qsharp/qre/_enumeration.py index b01d706944..17c065d12d 100644 --- a/source/pip/qsharp/qre/_enumeration.py +++ b/source/pip/qsharp/qre/_enumeration.py @@ -1,242 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -import types -from typing import ( - Generator, - Type, - TypeVar, - Literal, - Union, - cast, - get_args, - get_origin, - get_type_hints, -) -from dataclasses import MISSING -from itertools import product -from enum import Enum - - -T = TypeVar("T") - - -def _is_union_type(tp) -> bool: - """Check if a type is a Union or Python 3.10+ union (X | Y).""" - return get_origin(tp) is Union or isinstance(tp, types.UnionType) - - -def _is_type_filter(val, union_members: tuple) -> bool: - """ - Check if *val* is a union member type or a list of union member types, - i.e. a type filter for a union field (as opposed to a fixed value or - instance domain). - """ - member_set = set(union_members) - if isinstance(val, type) and val in member_set: - return True - if isinstance(val, list) and all( - isinstance(v, type) and v in member_set for v in val - ): - return True - return False - - -def _is_union_constraint_dict(val) -> bool: - """ - Check if *val* is a dict whose keys are all types, i.e. a per-member - constraint mapping for a union field. - - Example: ``{OptionA: {"number": [2, 3]}, OptionB: {}}`` - """ - return isinstance(val, dict) and all(isinstance(k, type) for k in val) - - -def _enumerate_union_members( - union_members: tuple, - val=None, -) -> list: - """ - Enumerate instances for a union-typed field. - - *val* controls which members are enumerated and how: - - - ``None`` - enumerate all members with their default domains. - - A single type (e.g. ``OptionB``) - enumerate only that member. - - A list of types (e.g. ``[OptionA, OptionB]``) - enumerate those members. - - A dict mapping types to constraint dicts - (e.g. ``{OptionA: {"number": [2, 3]}, OptionB: {}}``) - - enumerate only the listed members, forwarding the constraint dicts. - """ - # No override - enumerate all members with defaults - if val is None: - domain: list = [] - for member_type in union_members: - domain.extend(_enumerate_instances(member_type)) - return domain - - # Single type - if isinstance(val, type): - return list(_enumerate_instances(val)) - - # List of types - if isinstance(val, list) and all(isinstance(v, type) for v in val): - domain = [] - for member_type in val: - domain.extend(_enumerate_instances(member_type)) - return domain - - # Dict of type → constraint dict - if _is_union_constraint_dict(val): - domain = [] - for member_type, member_kwargs in cast(dict, val).items(): - domain.extend(_enumerate_instances(member_type, **member_kwargs)) - return domain - - raise ValueError( - f"Invalid value for union field: {val!r}. " - "Expected a union member type, a list of types, or a dict mapping " - "types to constraint dicts." - ) - - -def _enumerate_instances(cls: Type[T], **kwargs) -> Generator[T, None, None]: - """ - Yield all instances of a dataclass given its class. - - The enumeration logic supports defining domains for fields using the - ``domain`` metadata key. Additionally, boolean fields are automatically - enumerated with ``[True, False]``, Enum fields with all their members, - and Literal types with their defined values. - - **Nested dataclass fields** can be constrained by passing a dict:: - - _enumerate_instances(Outer, inner={"option": True}) - - **Union-typed fields** support several override forms: - - - A single type to select one member:: - - _enumerate_instances(Config, option=OptionB) - - - A list of types to select a subset:: - - _enumerate_instances(Config, option=[OptionA, OptionB]) - - - A dict mapping types to constraint dicts:: - - _enumerate_instances(Config, option={OptionA: {"number": [2, 3]}, OptionB: {}}) - - Args: - cls (Type[T]): The dataclass type to enumerate. - **kwargs: Fixed values or domains for fields. If a value is a list - and the corresponding field is kw_only, it is treated as a domain - to enumerate over. For nested dataclass fields a ``dict`` value - is forwarded as keyword arguments. For union-typed fields a type, - list of types, or ``dict[type, dict]`` controls member selection - and constraints. - - Returns: - Generator[T, None, None]: A generator yielding instances of the - dataclass. - - Raises: - ValueError: If a field cannot be enumerated (no domain found). - """ - - names = [] - values = [] - fixed_kwargs = {} - - if (fields := getattr(cls, "__dataclass_fields__", None)) is None: - # There are no fields defined for this class, so just yield a single - # instance - yield cls(**kwargs) - return - - # Resolve type hints to handle stringified types from __future__.annotations - type_hints = get_type_hints(cls) - - for field in fields.values(): # type: ignore - name = field.name - # Get resolved type or fallback to field.type - current_type = type_hints.get(name, field.type) - - if name in kwargs: - val = kwargs[name] - - is_union = _is_union_type(current_type) - union_members = get_args(current_type) if is_union else () - - # Union field with a type filter or constraint dict - if is_union and ( - _is_type_filter(val, union_members) or _is_union_constraint_dict(val) - ): - names.append(name) - values.append(_enumerate_union_members(union_members, val)) - continue - - # Nested dataclass field with a dict of constraints - if ( - isinstance(val, dict) - and not is_union - and isinstance(current_type, type) - and hasattr(current_type, "__dataclass_fields__") - ): - names.append(name) - values.append(list(_enumerate_instances(current_type, **val))) - continue - - # If kw_only and list, it's a domain to enumerate - if field.kw_only and isinstance(val, list): - names.append(name) - values.append(val) - else: - # Otherwise, it's a fixed value - fixed_kwargs[name] = val - continue - - if not field.kw_only: - # We don't enumerate non-kw-only fields that aren't in kwargs - continue - - # Derived domain logic - names.append(name) - - domain = field.metadata.get("domain", None) - if domain is not None: - values.append(domain) - continue - - if current_type is bool: - values.append([True, False]) - continue - - if isinstance(current_type, type) and issubclass(current_type, Enum): - values.append(list(current_type)) - continue - - if get_origin(current_type) is Literal: - values.append(list(get_args(current_type))) - continue - - # Union types (e.g., OptionA | OptionB or Union[OptionA, OptionB]) - if _is_union_type(current_type): - values.append(_enumerate_union_members(get_args(current_type), None)) - continue - - # Nested dataclass types - if isinstance(current_type, type) and hasattr( - current_type, "__dataclass_fields__" - ): - values.append(list(_enumerate_instances(current_type))) - continue - - if field.default is not MISSING: - values.append([field.default]) - continue - - raise ValueError(f"Cannot enumerate field {name}.") - - for instance_values in product(*values): - yield cls(**fixed_kwargs, **dict(zip(names, instance_values))) +# Deprecation shim – delegates to qdk.qre._enumeration +from qdk.qre._enumeration import * diff --git a/source/pip/qsharp/qre/_estimation.py b/source/pip/qsharp/qre/_estimation.py index 228e139ede..4daafc7141 100644 --- a/source/pip/qsharp/qre/_estimation.py +++ b/source/pip/qsharp/qre/_estimation.py @@ -1,218 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -from __future__ import annotations - -from typing import cast, Optional, Any - -from .. import telemetry_events -from ._application import Application -from ._architecture import Architecture -from ._qre import ( - _estimate_parallel, - _estimate_with_graph, - _EstimationCollection, - Trace, -) -from ._trace import TraceQuery, PSSPC, LatticeSurgery -from ._isa_enumeration import ISAQuery -from ._results import EstimationTable, EstimationTableEntry - - -def estimate( - application: Application, - architecture: Architecture, - isa_query: ISAQuery, - trace_query: Optional[TraceQuery] = None, - *, - max_error: float = 1.0, - post_process: bool = False, - use_graph: bool = True, - name: Optional[str] = None, -) -> EstimationTable: - """ - Estimate the resource requirements for a given application instance and - architecture. - - The application instance might return multiple traces. Each of the traces - is transformed by the trace query, which applies several trace transforms in - sequence. Each transform may return multiple traces. Similarly, the - architecture's ISA is transformed by the ISA query, which applies several - ISA transforms in sequence, each of which may return multiple ISAs. The - estimation is performed for each combination of transformed trace and ISA. - The results are collected into an EstimationTable and returned. - - The collection only contains the results that are optimal with respect to - the total number of qubits and the total runtime. - - Note: - The pruning strategy used when ``use_graph`` is set to True (default) - filters ISA instructions by comparing their per-instruction space, time, - and error independently. However, the total qubit count of a result - depends on the interaction between factory space and runtime: - ``factory_qubits = copies × factory_space`` where copies are determined - by ``count.div_ceil(runtime / factory_time)``. Because of this, an ISA - instruction that is dominated on per-instruction metrics can still - contribute to a globally Pareto-optimal result (e.g., a factory with - higher time may need fewer copies, leading to fewer total qubits). As a - consequence, ``use_graph=True`` may miss some results that - ``use_graph=False`` would find. Use ``use_graph=False`` when completeness of - the Pareto frontier is required. - - Args: - application (Application): The quantum application to be estimated. - architecture (Architecture): The target quantum architecture. - isa_query (ISAQuery): The ISA query to enumerate ISAs from the architecture. - trace_query (TraceQuery): The trace query to enumerate traces from the - application. - max_error (float): The maximum allowed error for the estimation results. - post_process (bool): If True, use the Python-threaded estimation path - (intended for future post-processing logic). If False (default), - use the Rust parallel estimation path. - use_graph (bool): If True (default), use the Rust estimation path that - builds a graph of ISAs and prunes suboptimal ISAs during estimation. - If False, use the Rust estimation path that does not perform any - pruning and simply enumerates all ISAs for each trace. - name (Optional[str]): An optional name for the estimation. If given, this - will be added as a first column to the results table for all entries. - - Returns: - EstimationTable: A table containing the optimal estimation results. - """ - - telemetry_events.on_qre_estimate(post_process=post_process, use_graph=use_graph) - - app_ctx = application.context() - arch_ctx = architecture.context() - - if trace_query is None: - trace_query = PSSPC.q() * LatticeSurgery.q() - - if post_process: - # Enumerate traces with their parameters so we can post-process later - params_and_traces = cast( - list[tuple[Any, Trace]], - list(trace_query.enumerate(app_ctx, track_parameters=True)), - ) - num_traces = len(params_and_traces) - - # Phase 1: Run all estimates in Rust (parallel, fast). - traces_only = [trace for _, trace in params_and_traces] - - if use_graph: - isa_query.populate(arch_ctx) - arch_ctx._provenance.build_pareto_index() - - num_isas = arch_ctx._provenance.total_isa_count() - - collection = _estimate_with_graph( - cast(list[Trace], traces_only), arch_ctx._provenance, max_error, True - ) - isas = collection.isas - else: - isas = list(isa_query.enumerate(arch_ctx)) - - num_isas = len(isas) - - collection = _estimate_parallel( - cast(list[Trace], traces_only), isas, max_error, True - ) - - total_jobs = collection.total_jobs - successful = collection.successful_estimates - summaries = collection.all_summaries # (trace_idx, isa_idx, qubits, runtime) - - # Phase 2: Learn per-trace runtime multiplier and qubit multiplier from - # one sample each: if post_process changes runtime or qubit count it - # will affect the Pareto optimality, but the changes depend only on the - # trace, not on the ISA. - trace_multipliers: dict[int, tuple[float, float]] = {} - trace_sample_isa: dict[int, int] = {} - for t_idx, isa_idx, _q, r in summaries: - if t_idx not in trace_sample_isa: - trace_sample_isa[t_idx] = isa_idx - for t_idx, isa_idx in trace_sample_isa.items(): - params, trace = params_and_traces[t_idx] - sample = trace.estimate(isas[isa_idx], max_error) - if sample is not None: - pre_q = sample.qubits - pre_r = sample.runtime - pp = app_ctx.application.post_process(params, sample) - if pp is not None and pre_r > 0 and pre_q > 0: - trace_multipliers[t_idx] = (pp.qubits / pre_q, pp.runtime / pre_r) - - # Phase 3: Estimate post-pp values and filter to Pareto candidates. - estimated_pp: list[tuple[int, int, int, int]] = ( - [] - ) # (t_idx, isa_idx, est_q, est_r) - for t_idx, isa_idx, q, r in summaries: - mult_q, mult_r = trace_multipliers.get(t_idx, (0.0, 0.0)) - est_q = int(q * mult_q) if mult_q > 0 else q - est_r = int(r * mult_r) if mult_r > 0 else r - estimated_pp.append((t_idx, isa_idx, est_q, est_r)) - - # Build approximate post-pp Pareto frontier to identify candidates. - estimated_pp.sort(key=lambda x: (x[2], x[3])) # sort by qubits, then runtime - approx_pareto: list[tuple[int, int, int, int]] = [] - min_r = float("inf") - for item in estimated_pp: - if item[3] < min_r: - approx_pareto.append(item) - min_r = item[3] - - # Phase 4: Re-estimate and post-process only the Pareto candidates. - pp_collection = _EstimationCollection() - for t_idx, isa_idx, _q, _r in approx_pareto: - params, trace = params_and_traces[t_idx] - result = trace.estimate(isas[isa_idx], max_error) - if result is not None: - pp_result = app_ctx.application.post_process(params, result) - if pp_result is not None: - pp_collection.insert(pp_result) - collection = pp_collection - else: - traces = list(trace_query.enumerate(app_ctx)) - num_traces = len(traces) - - if use_graph: - isa_query.populate(arch_ctx) - arch_ctx._provenance.build_pareto_index() - - num_isas = arch_ctx._provenance.total_isa_count() - - collection = _estimate_with_graph( - cast(list[Trace], traces), arch_ctx._provenance, max_error, False - ) - else: - isas = list(isa_query.enumerate(arch_ctx)) - - num_isas = len(isas) - - # Use the Rust parallel estimation path - collection = _estimate_parallel( - cast(list[Trace], traces), isas, max_error, False - ) - - total_jobs = collection.total_jobs - successful = collection.successful_estimates - - # Post-process the results and add them to a results table - table = EstimationTable() - - table.name = name - - if name is not None: - table.insert_column(0, "name", lambda entry: name) - - table.extend( - EstimationTableEntry.from_result(result, arch_ctx) for result in collection - ) - - # Fill in the stats for this estimation run - table.stats.num_traces = num_traces - table.stats.num_isas = num_isas - table.stats.total_jobs = total_jobs - table.stats.successful_estimates = successful - table.stats.pareto_results = len(collection) - - return table +# Deprecation shim – delegates to qdk.qre._estimation +from qdk.qre._estimation import * diff --git a/source/pip/qsharp/qre/_instruction.py b/source/pip/qsharp/qre/_instruction.py index 4669a86d4c..c4a762b14f 100644 --- a/source/pip/qsharp/qre/_instruction.py +++ b/source/pip/qsharp/qre/_instruction.py @@ -1,473 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -from __future__ import annotations - -from abc import ABC, abstractmethod -from dataclasses import dataclass, field -from typing import Generator, Iterable, Optional -from enum import IntEnum - -import pandas as pd - -from ._architecture import ISAContext, Architecture -from ._enumeration import _enumerate_instances -from ._isa_enumeration import ( - ISA_ROOT, - _BindingNode, - _ComponentQuery, - ISAQuery, -) -from ._qre import ( - ISA, - Constraint, - ConstraintBound, - Instruction, - ISARequirements, - instruction_name, - property_name_to_key, -) - - -class Encoding(IntEnum): - PHYSICAL = 0 - LOGICAL = 1 - - -PHYSICAL = Encoding.PHYSICAL -LOGICAL = Encoding.LOGICAL - - -def constraint( - id: int, - encoding: Encoding = PHYSICAL, - *, - arity: Optional[int] = 1, - error_rate: Optional[ConstraintBound] = None, - **kwargs: bool, -) -> Constraint: - """ - Create an instruction constraint. - - Args: - id (int): The instruction ID. - encoding (Encoding): The instruction encoding. PHYSICAL (0) or LOGICAL (1). - arity (Optional[int]): The instruction arity. If None, instruction is - assumed to have variable arity. Default is 1. - error_rate (Optional[ConstraintBound]): The constraint on the error rate. - **kwargs (bool): Required properties that matching instructions must have. - Valid property names: distance. Set to True to require the property. - - Returns: - Constraint: The instruction constraint. - - Raises: - ValueError: If an unknown property name is provided in kwargs. - """ - c = Constraint(id, encoding, arity, error_rate) - - for key, value in kwargs.items(): - if value: - if (prop_key := property_name_to_key(key)) is None: - raise ValueError(f"Unknown property '{key}'") - - c.add_property(prop_key) - - return c - - -class ISATransform(ABC): - """ - Abstract base class for transformations between ISAs (e.g., QEC schemes). - - An ISA transform defines a mapping from a required input ISA (e.g., - architecture constraints) to a provided output ISA (logical instructions). - It supports enumeration of configuration parameters. - """ - - @staticmethod - @abstractmethod - def required_isa() -> ISARequirements: - """ - Return the requirements that an implementation ISA must satisfy. - - Returns: - ISARequirements: The requirements for the underlying ISA. - """ - ... - - @abstractmethod - def provided_isa( - self, impl_isa: ISA, ctx: ISAContext - ) -> Generator[ISA, None, None]: - """ - Yields ISAs provided by this transform given an implementation ISA. - - Args: - impl_isa (ISA): The implementation ISA that satisfies requirements. - ctx (ISAContext): The enumeration context whose provenance graph - stores the instructions. - - Yields: - ISA: A provided logical ISA. - """ - ... - - @classmethod - def enumerate_isas( - cls, - impl_isa: ISA | Iterable[ISA], - ctx: ISAContext, - **kwargs, - ) -> Generator[ISA, None, None]: - """ - Enumerate all valid ISAs for this transform given implementation ISAs. - - This method iterates over all instances of the transform class (enumerating - hyperparameters) and filters implementation ISAs against requirements. - - Args: - impl_isa (ISA | Iterable[ISA]): One or more implementation ISAs. - ctx (ISAContext): The enumeration context. - **kwargs: Arguments passed to parameter enumeration. - - Yields: - ISA: Valid provided ISAs. - """ - isas = [impl_isa] if isinstance(impl_isa, ISA) else impl_isa - for isa in isas: - if not isa.satisfies(cls.required_isa()): - continue - - for component in _enumerate_instances(cls, **kwargs): - ctx._transforms[id(component)] = component - yield from component.provided_isa(isa, ctx) - - @classmethod - def q(cls, *, source: ISAQuery | None = None, **kwargs) -> ISAQuery: - """ - Create an ISAQuery node for this transform. - - Args: - source (Node | None): The source node providing implementation ISAs. - Defaults to ISA_ROOT. - **kwargs: Fixed values or domains for dataclass fields. Keyword-only - fields with a ``metadata["domain"]`` are enumerated automatically; - passing a value for such a field overrides or restricts the - domain. Non-keyword-only fields passed here are used as fixed - values for all enumerated instances. - - For example, given a transform with a non-keyword-only field - ``threshold`` and a keyword-only field ``distance`` with a - domain, calling ``MyTransform.q(threshold=0.03)`` fixes - ``threshold`` to 0.03 while still enumerating over all values - in the ``distance`` domain. - - Returns: - ISAQuery: An enumeration node representing this transform. - """ - return _ComponentQuery( - cls, source=source if source is not None else ISA_ROOT, kwargs=kwargs - ) - - @classmethod - def bind(cls, name: str, node: ISAQuery) -> _BindingNode: - """ - Create a BindingNode for this transform. - - This is a convenience method equivalent to ``cls.q().bind(name, node)``. - - Args: - name (str): The name to bind the transform's output to. - node (Node): The child node that can reference this binding. - - Returns: - BindingNode: A binding node enclosing this transform. - """ - return cls.q().bind(name, node) - - -@dataclass(slots=True) -class InstructionSource: - nodes: list[_InstructionSourceNode] = field(default_factory=list, init=False) - roots: list[int] = field(default_factory=list, init=False) - - @classmethod - def from_isa(cls, ctx: ISAContext, isa: ISA) -> InstructionSource: - """ - Construct an InstructionSource graph from an ISA. - - The instruction source graph contains more information than the - provenance graph in the context, as it connects the instructions to the - transforms and architectures that generated them. - - Args: - ctx (ISAContext): The enumeration context containing the provenance graph. - isa (ISA): Instructions in the ISA will serve as root nodes in the source graph. - - Returns: - InstructionSource: The instruction source graph for the estimation result. - """ - - def _make_node( - graph: InstructionSource, source_table: dict[int, int], source: int - ) -> int: - if source in source_table: - return source_table[source] - - children = [ - _make_node(graph, source_table, child) - for child in ctx._provenance.children(source) - if child != 0 - ] - - node = graph.add_node( - ctx._provenance.instruction(source), - ctx._transforms.get(ctx._provenance.transform_id(source)), - children, - ) - - source_table[source] = node - return node - - graph = cls() - source_table: dict[int, int] = {} - - for inst in isa: - node_idx = isa.node_index(inst.id) - if node_idx is not None and node_idx != 0: - node = _make_node(graph, source_table, node_idx) - graph.add_root(node) - - return graph - - def add_root(self, node_id: int) -> None: - """Add a root node to the instruction source graph. - - Args: - node_id (int): The index of the node to add as a root. - """ - self.roots.append(node_id) - - def add_node( - self, - instruction: Instruction, - transform: Optional[ISATransform | Architecture], - children: list[int], - ) -> int: - """Add a node to the instruction source graph. - - Args: - instruction (Instruction): The instruction for this node. - transform (Optional[ISATransform | Architecture]): The transform - that produced the instruction. - children (list[int]): Indices of child nodes. - - Returns: - int: The index of the newly added node. - """ - node_id = len(self.nodes) - self.nodes.append(_InstructionSourceNode(instruction, transform, children)) - return node_id - - def __str__(self) -> str: - """Return a formatted string representation of the instruction source graph.""" - - def _format_node(node: _InstructionSourceNode, indent: int = 0) -> str: - result = " " * indent + f"{instruction_name(node.instruction.id) or '??'}" - if node.transform is not None: - result += f" @ {node.transform}" - for child_index in node.children: - result += "\n" + _format_node(self.nodes[child_index], indent + 2) - return result - - return "\n".join( - _format_node(self.nodes[root_index]) for root_index in self.roots - ) - - def __getitem__(self, id: int) -> _InstructionSourceNodeReference: - """ - Retrieve the first instruction source root node with the given - instruction ID. Raises KeyError if no such node exists. - - Args: - id (int): The instruction ID to search for. - - Returns: - _InstructionSourceNodeReference: The first instruction source node with the - given instruction ID. - """ - if (node := self.get(id)) is not None: - return node - - raise KeyError(f"Instruction ID {id} not found in instruction source graph.") - - def __contains__(self, id: int) -> bool: - """ - Check if there is an instruction source root node with the given - instruction ID. - - Args: - id (int): The instruction ID to search for. - - Returns: - bool: True if a node with the given instruction ID exists, False otherwise. - """ - for root in self.roots: - if self.nodes[root].instruction.id == id: - return True - - return False - - def get( - self, id: int, default: Optional[_InstructionSourceNodeReference] = None - ) -> Optional[_InstructionSourceNodeReference]: - """ - Retrieve the first instruction source root node with the given - instruction ID. Returns default if no such node exists. - - Args: - id (int): The instruction ID to search for. - default (Optional[_InstructionSourceNodeReference]): The value to return if no - node with the given ID is found. Default is None. - - Returns: - Optional[_InstructionSourceNodeReference]: The first instruction source node with the - given instruction ID, or default if no such node exists. - """ - for root in self.roots: - if self.nodes[root].instruction.id == id: - return _InstructionSourceNodeReference(self, root) - - return default - - -@dataclass(frozen=True, slots=True) -class _InstructionSourceNode: - """A node in the instruction source graph.""" - - instruction: Instruction - transform: Optional[ISATransform | Architecture] - children: list[int] - - -class _InstructionSourceNodeReference: - """Reference to a node in an InstructionSource graph.""" - - def __init__(self, graph: InstructionSource, node_id: int): - """Initialize a reference to a node in the instruction source graph. - - Args: - graph (InstructionSource): The owning instruction source graph. - node_id (int): The index of the referenced node. - """ - self.graph = graph - self.node_id = node_id - - @property - def instruction(self) -> Instruction: - """The instruction at this node.""" - return self.graph.nodes[self.node_id].instruction - - @property - def transform(self) -> Optional[ISATransform | Architecture]: - """The transform that produced this node's instruction, if any.""" - return self.graph.nodes[self.node_id].transform - - def __str__(self) -> str: - """Return a string representation of the referenced node.""" - return str(self.graph.nodes[self.node_id]) - - def __getitem__(self, id: int) -> _InstructionSourceNodeReference: - """ - Retrieve the first child instruction source node with the given - instruction ID. Raises KeyError if no such node exists. - - Args: - id (int): The instruction ID to search for. - - Returns: - _InstructionSourceNodeReference: The first child instruction source node with the - given instruction ID. - """ - if (node := self.get(id)) is not None: - return node - - raise KeyError( - f"Instruction ID {id} not found in children of instruction {instruction_name(self.instruction.id) or '??'}." - ) - - def get( - self, id: int, default: Optional[_InstructionSourceNodeReference] = None - ) -> Optional[_InstructionSourceNodeReference]: - """ - Retrieve the first child instruction source node with the given - instruction ID. Returns default if no such node exists. - - Args: - id (int): The instruction ID to search for. - default (Optional[_InstructionSourceNodeReference]): The value to return if no - node with the given ID is found. Default is None. - - Returns: - Optional[_InstructionSourceNodeReference]: The first child instruction source - node with the given instruction ID, or default if no such node - exists. - """ - - for child_id in self.graph.nodes[self.node_id].children: - if self.graph.nodes[child_id].instruction.id == id: - return _InstructionSourceNodeReference(self.graph, child_id) - - return default - - -def _isa_as_frame(self: ISA) -> pd.DataFrame: - """Convert an ISA to a pandas DataFrame. - - Args: - self (ISA): The ISA to convert. - - Returns: - pd.DataFrame: A DataFrame with columns for id, encoding, arity, - space, time, and error. - """ - data = { - "id": [instruction_name(inst.id) for inst in self], - "encoding": [Encoding(inst.encoding).name for inst in self], - "arity": [inst.arity for inst in self], - "space": [ - inst.expect_space() if inst.arity is not None else None for inst in self - ], - "time": [ - inst.expect_time() if inst.arity is not None else None for inst in self - ], - "error": [ - inst.expect_error_rate() if inst.arity is not None else None - for inst in self - ], - } - - df = pd.DataFrame(data) - df.set_index("id", inplace=True) - return df - - -def _requirements_as_frame(self: ISARequirements) -> pd.DataFrame: - """Convert ISA requirements to a pandas DataFrame. - - Args: - self (ISARequirements): The requirements to convert. - - Returns: - pd.DataFrame: A DataFrame with columns for id, encoding, and arity. - """ - data = { - "id": [instruction_name(inst.id) for inst in self], - "encoding": [Encoding(inst.encoding).name for inst in self], - "arity": [inst.arity for inst in self], - } - - df = pd.DataFrame(data) - df.set_index("id", inplace=True) - return df +# Deprecation shim – delegates to qdk.qre._instruction +from qdk.qre._instruction import * diff --git a/source/pip/qsharp/qre/_isa_enumeration.py b/source/pip/qsharp/qre/_isa_enumeration.py index 7543c071ed..4552186044 100644 --- a/source/pip/qsharp/qre/_isa_enumeration.py +++ b/source/pip/qsharp/qre/_isa_enumeration.py @@ -1,428 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -from __future__ import annotations - -import functools -import itertools -from abc import ABC, abstractmethod -from dataclasses import dataclass, field -from typing import Generator - -from ._architecture import ISAContext -from ._enumeration import _enumerate_instances -from ._qre import ISA - - -class ISAQuery(ABC): - """ - Abstract base class for all nodes in the ISA enumeration tree. - - Enumeration nodes define the structure of the search space for ISAs starting - from architectures and modified by ISA transforms such as error correction - schemes. They can be composed using operators like ``+`` (sum) and ``*`` - (product) to build complex enumeration strategies. - """ - - @abstractmethod - def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: - """ - Yields all ISA instances represented by this enumeration node. - - Args: - ctx (ISAContext): The enumeration context containing shared state, - e.g., access to the underlying architecture. - - Yields: - ISA: A possible ISA that can be generated from this node. - """ - pass - - def populate(self, ctx: ISAContext) -> int: - """ - Populate the provenance graph with instructions from this node. - - Unlike ``enumerate``, this does not yield ISA objects. Each transform - queries the graph for Pareto-optimal instructions matching its - requirements, and adds produced instructions directly to the graph. - - Args: - ctx (ISAContext): The enumeration context whose provenance graph - will be populated. - - Returns: - int: The starting node index of the instructions contributed by - this subtree. Used by consumers to scope graph queries to only - see their source's nodes. - """ - # Default implementation: consume enumerate for its side effects - start = ctx._provenance.raw_node_count() - for _ in self.enumerate(ctx): - pass - return start - - def __add__(self, other: ISAQuery) -> _SumNode: - """ - Perform a union of two enumeration nodes. - - Enumerating the sum node yields all ISAs from this node, followed by all - ISAs from the other node. Duplicate ISAs may be produced if both nodes - yield the same ISA. - - Args: - other (Node): The other enumeration node. - - Returns: - SumNode: A node representing the union of both enumerations. - - Example: - - The following enumerates ISAs from both SurfaceCode and ColorCode: - - .. code-block:: python - for isa in SurfaceCode.q() + ColorCode.q(): - ... - """ - if isinstance(self, _SumNode) and isinstance(other, _SumNode): - sources = self.sources + other.sources - return _SumNode(sources) - elif isinstance(self, _SumNode): - sources = self.sources + [other] - return _SumNode(sources) - elif isinstance(other, _SumNode): - sources = [self] + other.sources - return _SumNode(sources) - else: - return _SumNode([self, other]) - - def __mul__(self, other: ISAQuery) -> _ProductNode: - """ - Perform the cross product of two enumeration nodes. - - Enumerating the product node yields ISAs resulting from the Cartesian - product of ISAs from both nodes. The ISAs are combined using - concatenation (logical union). This means that instructions in the - other enumeration node with the same ID as an instruction in this - enumeration node will overwrite the instruction from this node. - - Args: - other (Node): The other enumeration node. - - Returns: - ProductNode: A node representing the product of both enumerations. - - Example: - - The following enumerates ISAs formed by combining ISAs from a - surface code and a factory: - - .. code-block:: python - - for isa in SurfaceCode.q() * Factory.q(): - ... - """ - if isinstance(self, _ProductNode) and isinstance(other, _ProductNode): - sources = self.sources + other.sources - return _ProductNode(sources) - elif isinstance(self, _ProductNode): - sources = self.sources + [other] - return _ProductNode(sources) - elif isinstance(other, _ProductNode): - sources = [self] + other.sources - return _ProductNode(sources) - else: - return _ProductNode([self, other]) - - def bind(self, name: str, node: ISAQuery) -> "_BindingNode": - """Create a BindingNode with this node as the component. - - Args: - name: The name to bind the component to. - node: The child enumeration node that may contain ISARefNodes. - - Returns: - A BindingNode with self as the component. - - Example: - - .. code-block:: python - ExampleErrorCorrection.q().bind("c", ISARefNode("c") * ISARefNode("c")) - """ - return _BindingNode(name=name, component=self, node=node) - - -@dataclass -class RootNode(ISAQuery): - """ - Represents the architecture's base ISA. - Reads from the context instead of holding a reference. - """ - - def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: - """ - Yields the architecture ISA from the context. - - Args: - ctx (Context): The enumeration context. - - Yields: - ISA: The architecture's provided ISA, called root. - """ - yield ctx._isa - - def populate(self, ctx: ISAContext) -> int: - """Architecture ISA is already in the graph from ``ISAContext.__init__``. - - Returns: - int: 1, since architecture nodes start at index 1. - """ - return 1 - - -# Singleton instance for convenience -ISA_ROOT = RootNode() - - -@dataclass -class _ComponentQuery(ISAQuery): - """ - Query node that enumerates ISAs based on a component type and source. - - This node takes a component type (which must have an ``enumerate_isas`` class - method) and a source node. It enumerates the source node to get base ISAs, - and then calls ``enumerate_isas`` on the component type for each base ISA - to generate derived ISAs. - - Attributes: - component: The component type to query (e.g., a QEC code class). - source: The source node providing input ISAs (default: ISA_ROOT). - kwargs: Additional keyword arguments passed to ``enumerate_isas``. - """ - - component: type - source: ISAQuery = field(default_factory=lambda: ISA_ROOT) - kwargs: dict = field(default_factory=dict) - - def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: - """ - Yields ISAs generated by the component from source ISAs. - - Args: - ctx (Context): The enumeration context. - - Yields: - ISA: A generated ISA instance. - """ - for isa in self.source.enumerate(ctx): - yield from self.component.enumerate_isas(isa, ctx, **self.kwargs) - - def populate(self, ctx: ISAContext) -> int: - """ - Populate the graph by querying matching instructions. - - Runs the source first to ensure dependency instructions are in - the graph, then queries the graph for all instructions matching - this component's requirements within the source's node range. - For each matching ISA × each hyperparameter instance, calls - ``provided_isa`` to add new instructions to the graph. - - Returns: - int: The starting node index of this component's own additions. - """ - source_start = self.source.populate(ctx) - impl_isas = ctx._provenance.query_satisfying( - self.component.required_isa(), min_node_idx=source_start - ) - own_start = ctx._provenance.raw_node_count() - for instance in _enumerate_instances(self.component, **self.kwargs): - ctx._transforms[id(instance)] = instance - for impl_isa in impl_isas: - for _ in instance.provided_isa(impl_isa, ctx): - pass - return own_start - - -@dataclass -class _ProductNode(ISAQuery): - """ - Node representing the Cartesian product of multiple source nodes. - - Attributes: - sources: A list of source nodes to combine. - """ - - sources: list[ISAQuery] - - def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: - """ - Yields ISAs formed by combining ISAs from all source nodes. - - Args: - ctx (Context): The enumeration context. - - Yields: - ISA: A combined ISA instance. - """ - source_generators = [source.enumerate(ctx) for source in self.sources] - yield from ( - functools.reduce(lambda a, b: a + b, isa_tuple) - for isa_tuple in itertools.product(*source_generators) - ) - - def populate(self, ctx: ISAContext) -> int: - """Populate the graph from each source sequentially (no cross product). - - Returns: - int: The starting node index before any source populated. - """ - first = ctx._provenance.raw_node_count() - for source in self.sources: - source.populate(ctx) - return first - - -@dataclass -class _SumNode(ISAQuery): - """ - Node representing the union of multiple source nodes. - - Attributes: - sources: A list of source nodes to enumerate sequentially. - """ - - sources: list[ISAQuery] - - def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: - """ - Yields ISAs from each source node in sequence. - - Args: - ctx (Context): The enumeration context. - - Yields: - ISA: An ISA instance from one of the sources. - """ - for source in self.sources: - yield from source.enumerate(ctx) - - def populate(self, ctx: ISAContext) -> int: - """Populate the graph from each source sequentially. - - Returns: - int: The starting node index before any source populated. - """ - first = ctx._provenance.raw_node_count() - for source in self.sources: - source.populate(ctx) - return first - - -@dataclass -class ISARefNode(ISAQuery): - """ - A reference to a bound ISA in the enumeration context. - - This node looks up the binding from the context and yields the bound ISA. - - Args: - name: The name of the bound ISA to reference. - """ - - name: str - - def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: - """ - Yields the bound ISA from the context. - - Args: - ctx (Context): The enumeration context containing bindings. - - Yields: - ISA: The bound ISA. - - Raises: - ValueError: If the name is not bound in the context. - """ - if self.name not in ctx._bindings: - raise ValueError(f"Undefined component reference: '{self.name}'") - yield ctx._bindings[self.name] - - def populate(self, ctx: ISAContext) -> int: - """Instructions already in graph from the bound component. - - Returns: - int: 1, since bound component nodes start at index 1. - """ - return 1 - - -@dataclass -class _BindingNode(ISAQuery): - """ - Enumeration node that binds a component to a name. - - This node enables the as_/ref pattern where multiple positions in the - enumeration tree share the same component instance. The bound component - is enumerated once, and its value is shared across all ISARefNodes with - the same name via the context. - - For multiple bindings, nest BindingNode instances. - - Args: - name: The name to bind the component to. - component: An EnumerationNode (e.g., _ComponentQuery) that produces the bound ISAs. - node: The child enumeration node that may contain ISARefNodes. - - Example: - - .. code-block:: python - ctx = EnumerationContext(architecture=arch) - - # Bind a code and reference it multiple times - BindingNode( - name="c", - component=ExampleErrorCorrection.q(), - node=ISARefNode("c") * ISARefNode("c"), - ).enumerate(ctx) - - # Multiple bindings via nesting - BindingNode( - name="c", - component=ExampleErrorCorrection.q(), - node=BindingNode( - name="f", - component=ExampleFactory.q(source=ISARefNode("c")), - node=ISARefNode("c") * ISARefNode("f"), - ), - ).enumerate(ctx) - """ - - name: str - component: ISAQuery - node: ISAQuery - - def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: - """ - Enumerate child nodes with the bound component in context. - - Args: - ctx (Context): The enumeration context. - - Yields: - ISA: An ISA instance from the child node. - """ - # Enumerate all ISAs from the component node - for isa in self.component.enumerate(ctx): - # Add binding to context and enumerate child node - new_ctx = ctx._with_binding(self.name, isa) - yield from self.node.enumerate(new_ctx) - - def populate(self, ctx: ISAContext) -> int: - """Populate the graph from both the component and the child node. - - Returns: - int: The starting node index of the component's additions. - """ - comp_start = self.component.populate(ctx) - self.node.populate(ctx) - return comp_start +# Deprecation shim – delegates to qdk.qre._isa_enumeration +from qdk.qre._isa_enumeration import * diff --git a/source/pip/qsharp/qre/_qre.py b/source/pip/qsharp/qre/_qre.py index 2d1aaa7aa5..68a89c531c 100644 --- a/source/pip/qsharp/qre/_qre.py +++ b/source/pip/qsharp/qre/_qre.py @@ -1,36 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -# flake8: noqa E402 -# pyright: reportAttributeAccessIssue=false - -from .._native import ( - _binom_ppf, - block_linear_function, - Block, - constant_function, - Constraint, - ConstraintBound, - _estimate_parallel, - _estimate_with_graph, - _EstimationCollection, - EstimationResult, - FactoryResult, - _FloatFunction, - generic_function, - instruction_name, - Instruction, - InstructionFrontier, - _IntFunction, - ISA, - ISARequirements, - _ProvenanceGraph, - linear_function, - LatticeSurgery, - PSSPC, - Trace, - property_name_to_key, - property_name, - _float_to_bits, - _float_from_bits, -) +# Deprecation shim – delegates to qdk.qre._qre +from qdk.qre._qre import * diff --git a/source/pip/qsharp/qre/application/__init__.py b/source/pip/qsharp/qre/application/__init__.py index f6ee4c9f08..f47b24c95a 100644 --- a/source/pip/qsharp/qre/application/__init__.py +++ b/source/pip/qsharp/qre/application/__init__.py @@ -1,14 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -from ._cirq import CirqApplication -from ._qir import QIRApplication -from ._qsharp import QSharpApplication -from ._openqasm import OpenQASMApplication - -__all__ = [ - "CirqApplication", - "QIRApplication", - "QSharpApplication", - "OpenQASMApplication", -] +# Deprecation shim – delegates to qdk.qre.application +from qdk.qre.application import * diff --git a/source/pip/qsharp/qre/instruction_ids.py b/source/pip/qsharp/qre/instruction_ids.py index cec4a9c070..7cb87e21c0 100644 --- a/source/pip/qsharp/qre/instruction_ids.py +++ b/source/pip/qsharp/qre/instruction_ids.py @@ -1,10 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# pyright: reportAttributeAccessIssue=false - - -from .._native import instruction_ids - -for name in instruction_ids.__all__: - globals()[name] = getattr(instruction_ids, name) +# Deprecated: use qdk.qre.instruction_ids instead. +from qdk.qre.instruction_ids import * # noqa: F401,F403 diff --git a/source/pip/qsharp/qre/interop/__init__.py b/source/pip/qsharp/qre/interop/__init__.py index 52917a3a42..2ed7a9b952 100644 --- a/source/pip/qsharp/qre/interop/__init__.py +++ b/source/pip/qsharp/qre/interop/__init__.py @@ -1,35 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -from ._cirq import ( - PeakUsageGreedyQubitManager, - PopBlock, - PushBlock, - QubitType, - ReadFromMemoryGate, - TypedQubit, - WriteToMemoryGate, - assert_qubits_type, - read_from_memory, - trace_from_cirq, - write_to_memory, -) -from ._qir import trace_from_qir -from ._qsharp import trace_from_entry_expr, trace_from_entry_expr_cached - -__all__ = [ - "trace_from_cirq", - "trace_from_entry_expr", - "trace_from_entry_expr_cached", - "trace_from_qir", - "PushBlock", - "PopBlock", - "QubitType", - "TypedQubit", - "PeakUsageGreedyQubitManager", - "ReadFromMemoryGate", - "WriteToMemoryGate", - "write_to_memory", - "read_from_memory", - "assert_qubits_type", -] +# Deprecation shim – delegates to qdk.qre.interop +from qdk.qre.interop import * diff --git a/source/pip/qsharp/qre/interop/_cirq.py b/source/pip/qsharp/qre/interop/_cirq.py index 85808006ee..eae736aee9 100644 --- a/source/pip/qsharp/qre/interop/_cirq.py +++ b/source/pip/qsharp/qre/interop/_cirq.py @@ -1,822 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -from __future__ import annotations - -import random -from dataclasses import dataclass -from enum import Enum -from math import pi -from typing import Iterable, Iterator, Sequence, cast - -import cirq -from cirq import ( - CCXPowGate, - CCZPowGate, - ClassicallyControlledOperation, - CXPowGate, - CZPowGate, - GateOperation, - HPowGate, - MeasurementGate, - PhaseGradientGate, - ResetChannel, - SwapPowGate, - XPowGate, - YPowGate, - ZPowGate, -) - -from qsharp.qre import Block, Trace -from qsharp.qre.instruction_ids import ( - CCX, - CCZ, - CX, - CZ, - MEAS_Z, - PAULI_X, - PAULI_Y, - PAULI_Z, - READ_FROM_MEMORY, - RX, - RY, - RZ, - S_DAG, - SQRT_SQRT_X, - SQRT_SQRT_X_DAG, - SQRT_SQRT_Y, - SQRT_SQRT_Y_DAG, - SQRT_X, - SQRT_X_DAG, - SQRT_Y, - SQRT_Y_DAG, - SWAP, - T_DAG, - WRITE_TO_MEMORY, - H, - S, - T, -) - -_TOLERANCE = 1e-8 - - -def _approx_eq(a: float, b: float) -> bool: - """Check whether two floats are approximately equal.""" - return abs(a - b) <= _TOLERANCE - - -def trace_from_cirq( - circuit: cirq.CIRCUIT_LIKE, - *, - classical_control_probability: float = 0.5, - rotation_threshold: float = 1e-6, - track_memory_qubits: bool = True, -) -> Trace: - """Convert a Cirq circuit into a resource estimation Trace. - - Iterates through all moments and operations in the circuit, converting - each gate into trace operations. Gates with a ``_to_trace`` method are - converted directly; others are recursively decomposed via Cirq's - ``_decompose_with_context_`` or ``_decompose_`` protocols. - - Args: - circuit: The Cirq circuit to convert. - classical_control_probability: Probability that a classically - controlled operation is included in the trace. Defaults to 0.5. - rotation_threshold: Rotation exponents with absolute value below - this threshold are treated as identity and omitted from the - trace. This applies to single-qubit rotations (RX, RY, RZ) as - well as to the rotation components of controlled-Z - decompositions. Defaults to 1e-6. - track_memory_qubits (bool): When True, memory qubits are tracked - separately from compute qubits. When False, all qubits are treated - as compute qubits. Also, if True, read-from-memory and - write-to-memory instructions are preserved in the trace, otherwise, - they are decompsed into SWAP and RESET instructions. Defaults to - True. - - Returns: - Trace: A Trace representing an execution profile of the circuit. - """ - - if isinstance(circuit, cirq.Circuit): - # circuit is already in the expected format, so we can process it directly. - pass - elif isinstance(circuit, cirq.Gate): - circuit = cirq.Circuit(circuit.on(*cirq.LineQid.for_gate(circuit))) - else: - # circuit is OP_TREE - circuit = cirq.Circuit(circuit) - - context = _CirqTraceBuilder( - circuit, classical_control_probability, rotation_threshold, track_memory_qubits - ) - - for moment in circuit: - for op in moment.operations: - context.handle_op(op) - - return context.trace - - -class _CirqTraceBuilder: - """Builds a resource estimation ``Trace`` from a Cirq circuit. - - This class walks the operations produced by ``trace_from_cirq`` and - translates each one into trace instructions. It maintains the state - needed during the conversion: - - * A ``Trace`` instance that accumulates the result. - * A stack of ``Block`` objects so that ``PushBlock`` / ``PopBlock`` - markers can create nested repeated sections. - * A qubit-id mapping (``_QidToTraceId``) that assigns each Cirq qubit - a sequential integer index. - * A Cirq ``DecompositionContext`` for gates that need recursive - decomposition. - - Args: - circuit: The Cirq circuit being converted. - classical_control_probability: Probability that a classically - controlled operation is included in the trace. - rotation_threshold: Rotation exponents with absolute value below - this threshold are treated as identity. - """ - - def __init__( - self, - circuit: cirq.Circuit, - classical_control_probability: float, - rotation_threshold: float, - track_memory_qubits: bool = True, - ): - self._circuit = circuit - self._trace = Trace(0) - self._classical_control_probability = classical_control_probability - self._rotation_threshold = rotation_threshold - self._track_memory_qubits = track_memory_qubits - self._blocks = [self._trace.root_block()] - self._q_to_id = _QidToTraceId(circuit.all_qubits()) - self._decomp_context = cirq.DecompositionContext( - qubit_manager=PeakUsageGreedyQubitManager( - "trace_from_cirq", size=0, maximize_reuse=True - ) - ) - - def push_block(self, repetitions: int): - """Open a new repeated block with the given number of repetitions.""" - block = self.block.add_block(repetitions) - self._blocks.append(block) - - def pop_block(self): - """Close the current repeated block, returning to the parent.""" - self._blocks.pop() - - @property - def trace(self) -> Trace: - """Determine compute and memory qubits from the circuit's qubits as well - as from the qubit manager before returning the trace.""" - - qm = cast(PeakUsageGreedyQubitManager, self._decomp_context.qubit_manager) - num_memory_qubits, num_compute_qubits = 0, 0 - - for q in self._circuit.all_qubits(): - if ( - self._track_memory_qubits - and isinstance(q, TypedQubit) - and q.qubit_type == QubitType.MEMORY - ): - num_memory_qubits += 1 - else: - # Untyped qubits are considered COMPUTE by default. - num_compute_qubits += 1 - - if self._track_memory_qubits: - num_memory_qubits += qm.memory_qubit_count() - else: - num_compute_qubits += qm.memory_qubit_count() - num_compute_qubits += qm.compute_qubit_count() - - self._trace.compute_qubits = num_compute_qubits - if self._track_memory_qubits and num_memory_qubits > 0: - self._trace.memory_qubits = num_memory_qubits - - return self._trace - - @property - def block(self) -> Block: - """The innermost open block in the trace.""" - return self._blocks[-1] - - @property - def q_to_id(self) -> _QidToTraceId: - """Mapping from Cirq ``Qid`` to integer trace qubit index.""" - return self._q_to_id - - @property - def classical_control_probability(self) -> float: - """Probability used to stochastically include classically controlled - operations.""" - return self._classical_control_probability - - @property - def rotation_threshold(self) -> float: - """Rotation exponents with absolute value below this threshold are - treated as identity.""" - return self._rotation_threshold - - @property - def decomp_context(self) -> cirq.DecompositionContext: - """Cirq decomposition context shared across all recursive - decompositions.""" - return self._decomp_context - - def handle_op( - self, - op: cirq.OP_TREE | TraceGate | PushBlock | PopBlock, - ) -> None: - """Recursively convert a single operation into trace instructions. - - Supported operation forms: - - - ``TraceGate``: A raw trace instruction, added directly to the - current block. - - ``PushBlock`` / ``PopBlock``: Control block nesting with - repetitions. - - ``GateOperation``: Dispatched via ``_to_trace`` if available on - the gate, otherwise decomposed via - ``_decompose_with_context_`` or ``_decompose_``. - - ``ClassicallyControlledOperation``: Included with the probability - given by ``classical_control_probability``. - - ``list`` / iterable: Each element is handled recursively. - - Any other ``cirq.Operation``: Decomposed via - ``_decompose_with_context_``. - - Args: - op: The operation to convert. - """ - if isinstance(op, TraceGate): - qs = [ - self.q_to_id[q] - for q in ([op.qubits] if isinstance(op.qubits, cirq.Qid) else op.qubits) - ] - - if op.params is None: - self.block.add_operation(op.id, qs) - else: - self.block.add_operation( - op.id, qs, op.params if isinstance(op.params, list) else [op.params] - ) - elif isinstance(op, PushBlock): - self.push_block(op.repetitions) - elif isinstance(op, PopBlock): - self.pop_block() - elif isinstance(op, cirq.Operation): - if isinstance(op, GateOperation): - gate = op.gate - - if hasattr(gate, "_to_trace"): - for sub_op in gate._to_trace(self, op): # type: ignore - self.handle_op(sub_op) - elif hasattr(gate, "_decompose_with_context_"): - for sub_op in gate._decompose_with_context_(op.qubits, self.decomp_context): # type: ignore - self.handle_op(sub_op) - elif hasattr(gate, "_decompose_"): - # decompose the gate and handle the resulting operations recursively - for sub_op in gate._decompose_(op.qubits): # type: ignore - self.handle_op(sub_op) - else: - for sub_op in op._decompose_with_context_(self.decomp_context): # type: ignore - self.handle_op(sub_op) - elif isinstance(op, ClassicallyControlledOperation): - if random.random() < self.classical_control_probability: - self.handle_op(op.without_classical_controls()) - elif isinstance(op, cirq.CircuitOperation): - if isinstance(op.repetitions, int): - self.push_block(op.repetitions) - for sub_op in op.circuit: # type: ignore - self.handle_op(sub_op) - self.pop_block() - else: - raise ValueError( - "Only integer repetitions are supported for CircuitOperation." - ) - else: - for sub_op in op._decompose_with_context_(self.decomp_context): # type: ignore - self.handle_op(sub_op) - else: - # op is Iterable[OP_TREE] - for sub_op in op: - self.handle_op(sub_op) - - -@dataclass(frozen=True, slots=True) -class PushBlock: - """Signals the start of a repeated block in the trace. - - Args: - repetitions: Number of times the block is repeated. - """ - - repetitions: int - - -@dataclass(frozen=True, slots=True) -class PopBlock: - """Signals the end of the current repeated block in the trace.""" - - ... - - -@dataclass(frozen=True, slots=True) -class TraceGate: - """A raw trace instruction emitted during Cirq circuit conversion. - - Attributes: - id (int): The instruction ID. - qubits (list[cirq.Qid] | cirq.Qid): The target qubits. - params (list[float] | float | None): Optional gate parameters. - """ - - id: int - qubits: list[cirq.Qid] | cirq.Qid - params: list[float] | float | None = None - - -class _QidToTraceId(dict): - """Mapping from Cirq qubits to integer trace qubit indices. - - Initialized with a set of known qubits. If an unknown qubit is looked - up, it is automatically assigned the next available index. - """ - - def __init__(self, init: Iterable[cirq.Qid]): - super().__init__({q: i for i, q in enumerate(init)}) - - def __getitem__(self, key: cirq.Qid) -> int: - """ - If the key is not present, add it to the mapping with the next available id. - """ - - if key not in self: - self[key] = len(self) - return super().__getitem__(key) - - -def h_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): - """Convert an HPowGate into trace instructions.""" - if _approx_eq(abs(self.exponent), 1): - yield TraceGate(H, [op.qubits[0]]) - else: - yield from op._decompose_with_context_(context.decomp_context) # type: ignore - - -def x_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): - """Convert an XPowGate into trace instructions.""" - q = [op.qubits[0]] - exp = self.exponent - if _approx_eq(exp, 1) or _approx_eq(exp, -1): - yield TraceGate(PAULI_X, q) - elif _approx_eq(exp, 0.5): - yield TraceGate(SQRT_X, q) - elif _approx_eq(exp, -0.5): - yield TraceGate(SQRT_X_DAG, q) - elif _approx_eq(exp, 0.25): - yield TraceGate(SQRT_SQRT_X, q) - elif _approx_eq(exp, -0.25): - yield TraceGate(SQRT_SQRT_X_DAG, q) - else: - if abs(exp) >= context.rotation_threshold: - yield TraceGate(RX, q, exp * pi) - - -def y_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): - """Convert a YPowGate into trace instructions.""" - q = [op.qubits[0]] - exp = self.exponent - if _approx_eq(exp, 1) or _approx_eq(exp, -1): - yield TraceGate(PAULI_Y, q) - elif _approx_eq(exp, 0.5): - yield TraceGate(SQRT_Y, q) - elif _approx_eq(exp, -0.5): - yield TraceGate(SQRT_Y_DAG, q) - elif _approx_eq(exp, 0.25): - yield TraceGate(SQRT_SQRT_Y, q) - elif _approx_eq(exp, -0.25): - yield TraceGate(SQRT_SQRT_Y_DAG, q) - else: - if abs(exp) >= context.rotation_threshold: - yield TraceGate(RY, q, exp * pi) - - -def z_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): - """Convert a ZPowGate into trace instructions.""" - q = [op.qubits[0]] - exp = self.exponent - if _approx_eq(exp, 1) or _approx_eq(exp, -1): - yield TraceGate(PAULI_Z, q) - elif _approx_eq(exp, 0.5): - yield TraceGate(S, q) - elif _approx_eq(exp, -0.5): - yield TraceGate(S_DAG, q) - elif _approx_eq(exp, 0.25): - yield TraceGate(T, q) - elif _approx_eq(exp, -0.25): - yield TraceGate(T_DAG, q) - else: - if abs(exp) >= context.rotation_threshold: - yield TraceGate(RZ, q, exp * pi) - - -def cx_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): - """Convert a CXPowGate into trace instructions.""" - if _approx_eq(abs(self.exponent), 1): - yield TraceGate(CX, [op.qubits[0], op.qubits[1]]) - else: - yield from op._decompose_with_context_(context.decomp_context) # type: ignore - - -def cz_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): - """Convert a CZPowGate into trace instructions.""" - exp = self.exponent - c, t = op.qubits[0], op.qubits[1] - if _approx_eq(abs(exp), 1): - yield TraceGate(CZ, [c, t]) - elif _approx_eq(exp, 0.5): - # controlled S gate - yield TraceGate(T, [c]) - yield TraceGate(T, [t]) - yield TraceGate(CZ, [c, t]) - yield TraceGate(T_DAG, [t]) - yield TraceGate(CZ, [c, t]) - elif _approx_eq(exp, -0.5): - # controlled S† gate - yield TraceGate(T_DAG, [c]) - yield TraceGate(T_DAG, [t]) - yield TraceGate(CZ, [c, t]) - yield TraceGate(T, [t]) - yield TraceGate(CZ, [c, t]) - else: - half_exp = exp / 2 - if abs(half_exp) >= context.rotation_threshold: - rads = half_exp * pi - yield TraceGate(RZ, [c], [rads]) - yield TraceGate(RZ, [t], [rads]) - yield TraceGate(CZ, [c, t]) - yield TraceGate(RZ, [t], [-rads]) - yield TraceGate(CZ, [c, t]) - - -def swap_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): - """Convert a SwapPowGate into trace instructions.""" - if _approx_eq(abs(self.exponent), 1): - yield TraceGate(SWAP, [op.qubits[0], op.qubits[1]]) - else: - yield from op._decompose_with_context_(context.decomp_context) # type: ignore - - -def ccx_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): - """Convert a CCXPowGate into trace instructions.""" - if _approx_eq(abs(self.exponent), 1): - yield TraceGate(CCX, [op.qubits[0], op.qubits[1], op.qubits[2]]) - else: - yield from op._decompose_with_context_(context.decomp_context) # type: ignore - - -def ccz_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): - """Convert a CCZPowGate into trace instructions.""" - if _approx_eq(abs(self.exponent), 1): - yield TraceGate(CCZ, [op.qubits[0], op.qubits[1], op.qubits[2]]) - else: - yield from op._decompose_with_context_(context.decomp_context) # type: ignore - - -def measurement_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): - """Convert a MeasurementGate into trace instructions.""" - for q in op.qubits: - yield TraceGate(MEAS_Z, [q]) - - -def reset_channel_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): - """Convert a ResetChannel into trace instructions (no-op).""" - yield from () - - -# Attach _to_trace methods to Cirq gate classes so that handle_op can -# convert them directly into trace instructions without decomposition. -HPowGate._to_trace = h_pow_gate_to_trace -XPowGate._to_trace = x_pow_gate_to_trace -YPowGate._to_trace = y_pow_gate_to_trace -ZPowGate._to_trace = z_pow_gate_to_trace -CXPowGate._to_trace = cx_pow_gate_to_trace -CZPowGate._to_trace = cz_pow_gate_to_trace -SwapPowGate._to_trace = swap_pow_gate_to_trace -CCXPowGate._to_trace = ccx_pow_gate_to_trace -CCZPowGate._to_trace = ccz_pow_gate_to_trace -MeasurementGate._to_trace = measurement_gate_to_trace -ResetChannel._to_trace = reset_channel_to_trace - -# Decomposition overrides - - -def phase_gradient_decompose(self, qubits): - """Override PhaseGradientGate._decompose_ to skip rotations with very small angles. - - The original implementation may lead to floating-point overflows for - large values of i. - """ - - for i, q in enumerate(qubits): - exp = self.exponent / 2**i - if abs(exp) < 1e-6: - break - yield cirq.Z(q) ** exp - - -PhaseGradientGate._decompose_ = phase_gradient_decompose - - -class QubitType(Enum): - """Qubit type. - - Each logical qubit can be either a compute or memory qubit. Compute qubits - can be used normally. - - Memory qubits have a restriction that gates cannot be applied to them. The - only allowed operations on memory qubits are reads/writes, where state is - moved from memory to compute gate or from compute to memory gate. - - We assume that when error correction is applied, memory qubits are encoded - with a more efficient error correction scheme requiring less resources, but - not allowing gate application (e.g. Yoked surface codes, - https://arxiv.org/abs/2312.04522). - """ - - COMPUTE = 1 - MEMORY = 2 - - -class TypedQubit(cirq.Qid): - """Qubit with type.""" - - def __init__( - self, - qubit: cirq.Qid, - qubit_type: QubitType, - ): - """Initializes typed qubit.""" - self._qubit = qubit - self.qubit_type = qubit_type - - def _comparison_key(self) -> object: - """Comparison key.""" - return self._qubit._comparison_key() - - @property - def dimension(self) -> int: - """Dimension.""" - return cast("int", self._qubit.dimension) - - def __repr__(self) -> str: - """String representation of the qubit.""" - return repr(self._qubit) - - -def _as_typed_qubit(q: cirq.Qid) -> TypedQubit: - """Converts qubit to TypedQubit.""" - assert isinstance(q, TypedQubit) - return q - - -def assert_qubits_type(qs: Sequence[cirq.Qid], qubit_type: QubitType) -> None: - """Asserts that qubits have specified type, but only if they are TypedQubits.""" - if len(qs) == 0 or not isinstance(qs[0], TypedQubit): - return - - for q in qs: - actual_type = _as_typed_qubit(q).qubit_type - assert ( - actual_type == qubit_type - ), f"{q} expected to be {qubit_type}, was {actual_type}." - - -class _TypedQubitManager(cirq.GreedyQubitManager): - """Qubit manager managing qubits of specified type. - - All allocated qubits will have specified type. - Tracks current and peak number of qubits. - """ - - def __init__( - self, prefix: str, qubit_type: QubitType, *, size: int, maximize_reuse: bool - ): - """Initialize the manager.""" - prefix = prefix + "_" + qubit_type.name[0] - super().__init__(prefix, size=size, maximize_reuse=maximize_reuse) - self.qubit_type = qubit_type - self.current_in_use = 0 - self.peak_in_use = 0 - - def _allocate_qid(self, name: str, dim: int) -> cirq.Qid: - """Allocates single qubit.""" - return TypedQubit(super()._allocate_qid(name, dim), self.qubit_type) - - def qalloc(self, n: int, dim: int) -> list[cirq.Qid]: - """Allocate ``n`` qubits and update the usage counters.""" - qs = super().qalloc(n, dim) - self.current_in_use += len(qs) - self.peak_in_use = max(self.peak_in_use, self.current_in_use) - return cast("list[cirq.Qid]", qs) - - def qfree(self, qubits: Iterable[cirq.Qid]) -> None: - """Free the given qubits and update the usage counters.""" - super().qfree(qubits) - self.current_in_use -= len(set(qubits)) - - -class PeakUsageGreedyQubitManager(cirq.QubitManager): - """A qubit manager tracking compute and memory qubits separately. - - It consists of two independent qubit managers for each qubit type. Each manager - uses greedy allocation strategy from ``cirq.GreedyQubitManager``. - - Qubits of one type, after freed, cannot be reused as qubits of different type. - Therefore, peak qubit count is equal to sum of peak qubit counts for each type. - """ - - def __init__(self, prefix: str, *, size: int, maximize_reuse: bool): - """Initialize the PeakUsageGreedyQubitManager. - - Args: - prefix: Naming prefix for allocated qubits. - size: Initial pool size passed through to ``cirq.GreedyQubitManager``. - Example: 0. - maximize_reuse: Flag to control qubit reuse strategy. If ``False``, this - mode uses a FIFO (First in First out) strategy s.t. next allocated qubit - is one which was freed the earliest. If ``True``, this mode uses a LIFO - (Last in First out) strategy s.t. the next allocated qubit is one which - was freed the latest. - - """ - self.typed_managers = { - qubit_type: _TypedQubitManager( - prefix, qubit_type, size=size, maximize_reuse=maximize_reuse - ) - for qubit_type in QubitType - } - - def qalloc( - self, n: int, dim: int, qubit_type: QubitType = QubitType.COMPUTE - ) -> list[cirq.Qid]: - """Allocate ``n`` qubits and update the usage counters. - - Args: - n: Number of qubits to allocate. - dim: Dimension of each qubit. Example: 2 for qubits. - qubit_type: Type of qubits (COMPUTE or MEMORY). - - Returns: - List of allocated qubits. - - """ - return self.typed_managers[qubit_type].qalloc(n, dim) - - def qborrow(self, n: int, dim: int = 2) -> list[cirq.Qid]: - """Borrow qubits (not supported).""" - raise NotImplementedError("qborrow is not supported.") - - def qfree(self, qubits: Iterable[cirq.Qid]) -> None: - """Free the given qubits.""" - qubits_by_type: dict[QubitType, list[cirq.Qid]] = {t: [] for t in QubitType} - for q in qubits: - qubits_by_type[_as_typed_qubit(q).qubit_type].append(q) - for qubit_type, qs in qubits_by_type.items(): - if len(qs) > 0: - self.typed_managers[qubit_type].qfree(qs) - - def current_in_use(self) -> int: - """Number of qubits currently in use.""" - return sum(qm.current_in_use for qm in self.typed_managers.values()) - - def qubit_count(self) -> int: - """Returns the peak number of qubits of all types. - - It is equal to sum of peak counts for each type, because qubits of one type - cannot be reused as qubits of a different type. - """ - return self.compute_qubit_count() + self.memory_qubit_count() - - def compute_qubit_count(self) -> int: - """Returns the peak number of simultaneously in-use COMPUTE qubits.""" - return self.typed_managers[QubitType.COMPUTE].peak_in_use - - def memory_qubit_count(self) -> int: - """Returns the peak number of simultaneously in-use MEMORY qubits.""" - return self.typed_managers[QubitType.MEMORY].peak_in_use - - -class ReadFromMemoryGate(cirq.Gate): - """Moves qubit states from MEMORY register to COMPUTE register. - - Assumes COMPUTE qubits are prepared in 0 state. Leaves MEMORY qubits in 0 state. - """ - - def __init__(self, n: int): - """Initializes ReadFromMemoryGate.""" - self.n = n - - def _num_qubits_(self) -> int: - """Number of qubits passed in to this gate.""" - return 2 * self.n - - def _decompose_(self, qubits: Sequence[cirq.Qid]) -> Iterator[cirq.Operation]: - """Decomposes this gate into equivalent SWAP gates.""" - comp_qs, mem_qs = self._get_qubits(qubits) - for i in range(self.n): - yield cirq.reset(comp_qs[i]) - yield cirq.SWAP(mem_qs[i], comp_qs[i]) - - def _to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation, **_kwargs): - """Convert this gate into trace instructions.""" - if context._track_memory_qubits: - comp_qs, mem_qs = self._get_qubits(op.qubits) - for i in range(self.n): - yield TraceGate(READ_FROM_MEMORY, [mem_qs[i], comp_qs[i]]) - else: - yield from self._decompose_(op.qubits) - - def _get_qubits( - self, qubits: Sequence[cirq.Qid] - ) -> tuple[Sequence[cirq.Qid], Sequence[cirq.Qid]]: - """Get qubits for this gate partitioned into compute and memory - qubits.""" - assert len(qubits) == 2 * self.n - mem_qs = qubits[0 : self.n] - comp_qs = qubits[self.n : 2 * self.n] - assert_qubits_type(mem_qs, QubitType.MEMORY) - assert_qubits_type(comp_qs, QubitType.COMPUTE) - return comp_qs, mem_qs - - -class WriteToMemoryGate(cirq.Gate): - """Moves qubit states from COMPUTE register to MEMORY register. - - Assumes MEMORY qubits are prepared in 0 state. Leaves COMPUTE qubits in 0 state. - """ - - def __init__(self, n: int): - """Initializes WriteToMemoryGate.""" - self.n = n - - def _num_qubits_(self) -> int: - """Number of qubits passed in to this gate.""" - return 2 * self.n - - def _decompose_(self, qubits: Sequence[cirq.Qid]) -> Iterator[cirq.Operation]: - """Decomposes this gate into equivalent SWAP gates.""" - comp_qs, mem_qs = self._get_qubits(qubits) - for i in range(self.n): - yield cirq.reset(mem_qs[i]) - yield cirq.SWAP(mem_qs[i], comp_qs[i]) - - def _to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation, **_kwargs): - """Convert this gate into trace instructions.""" - if context._track_memory_qubits: - comp_qs, mem_qs = self._get_qubits(op.qubits) - for i in range(self.n): - yield TraceGate(WRITE_TO_MEMORY, [comp_qs[i], mem_qs[i]]) - else: - yield from self._decompose_(op.qubits) - - def _get_qubits( - self, qubits: Sequence[cirq.Qid] - ) -> tuple[Sequence[cirq.Qid], Sequence[cirq.Qid]]: - assert len(qubits) == 2 * self.n - mem_qs = qubits[0 : self.n] - comp_qs = qubits[self.n : 2 * self.n] - assert_qubits_type(mem_qs, QubitType.MEMORY) - assert_qubits_type(comp_qs, QubitType.COMPUTE) - - return comp_qs, mem_qs - - -def write_to_memory( - memory_qubits: Sequence[cirq.Qid], compute_qubits: Sequence[cirq.Qid] -) -> cirq.Operation: - """Operation to write qubits to memory.""" - assert_qubits_type(memory_qubits, QubitType.MEMORY) - assert_qubits_type(compute_qubits, QubitType.COMPUTE) - n = len(memory_qubits) - assert n == len(compute_qubits) - return WriteToMemoryGate(n).on(*memory_qubits, *compute_qubits) - - -def read_from_memory( - memory_qubits: Sequence[cirq.Qid], compute_qubits: Sequence[cirq.Qid] -) -> cirq.Operation: - """Operation to read qubits from memory.""" - assert_qubits_type(memory_qubits, QubitType.MEMORY) - assert_qubits_type(compute_qubits, QubitType.COMPUTE) - n = len(memory_qubits) - assert n == len(compute_qubits) - return ReadFromMemoryGate(n).on(*memory_qubits, *compute_qubits) +# Deprecation shim – delegates to qdk.qre.interop._cirq +from qdk.qre.interop._cirq import * diff --git a/source/pip/qsharp/qre/models/__init__.py b/source/pip/qsharp/qre/models/__init__.py index 3da76797ac..a84ac70e66 100644 --- a/source/pip/qsharp/qre/models/__init__.py +++ b/source/pip/qsharp/qre/models/__init__.py @@ -1,23 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -from .factories import Litinski19Factory, MagicUpToClifford, RoundBasedFactory -from .qec import ( - SurfaceCode, - ThreeAux, - OneDimensionalYokedSurfaceCode, - TwoDimensionalYokedSurfaceCode, -) -from .qubits import GateBased, Majorana - -__all__ = [ - "GateBased", - "Litinski19Factory", - "Majorana", - "MagicUpToClifford", - "RoundBasedFactory", - "SurfaceCode", - "ThreeAux", - "OneDimensionalYokedSurfaceCode", - "TwoDimensionalYokedSurfaceCode", -] +# Deprecation shim – delegates to qdk.qre.models +from qdk.qre.models import * diff --git a/source/pip/qsharp/qre/property_keys.py b/source/pip/qsharp/qre/property_keys.py index 917e25ca0d..0d2afc07f4 100644 --- a/source/pip/qsharp/qre/property_keys.py +++ b/source/pip/qsharp/qre/property_keys.py @@ -1,10 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# pyright: reportAttributeAccessIssue=false - - -from .._native import property_keys - -for name in property_keys.__all__: - globals()[name] = getattr(property_keys, name) +# Deprecated: use qdk.qre.property_keys instead. +from qdk.qre.property_keys import * # noqa: F401,F403 diff --git a/source/pip/qsharp/telemetry.py b/source/pip/qsharp/telemetry.py index 4114c69878..ca22eb5c38 100644 --- a/source/pip/qsharp/telemetry.py +++ b/source/pip/qsharp/telemetry.py @@ -1,310 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -""" -This module sends telemetry directly to Azure Monitor using a similar mechanism and -format to the Azure Monitor OpenTelemetry Python SDK. It only supports custom metrics of -type "counter" and "histogram" for now. It's goal is to be minimal in size and dependencies, -and easy to read to understand exactly what data is being sent. - -To use this API, simply call `log_telemetry` with the metric name, value, and any other -optional properties. The telemetry will be batched and sent at a regular intervals (60 sec), -and when the process is about to exit. - -Disable qsharp Python telemetry by setting the environment variable `QSHARP_PYTHON_TELEMETRY=none`. -""" - -import atexit -import json -import locale -import logging -import os -import platform -import time -import urllib.request -import warnings - -from datetime import datetime, timezone -from queue import SimpleQueue, Empty -from threading import Thread -from typing import Any, Dict, Literal, List, TypedDict, Union - -logger = logging.getLogger(__name__) - -QSHARP_VERSION = "0.0.0.dev0" - -AIKEY = os.environ.get("QSHARP_PYTHON_AI_KEY") or "95d25b22-8b6d-448e-9677-78ad4047a95a" -AIURL = ( - os.environ.get("QSHARP_PYTHON_AI_URL") - or "https://westus2-2.in.applicationinsights.azure.com//v2.1/track" -) - -# If explicitly disabled via either environment variable, disable telemetry. This takes precedence. -# If explicitly enabled via either environment variable, enable telemetry. -# Otherwise, enable telemetry only in release builds. -_disable_values = {"0", "false", "disabled", "none"} -_enable_values = {"1", "true", "enabled"} -_env_values = { - (os.environ.get("QSHARP_PYTHON_TELEMETRY") or "").lower(), - (os.environ.get("QDK_PYTHON_TELEMETRY") or "").lower(), -} - -# `&` here is set intersection: it yields the common values between sets. -# `not _env_values & _disable_values` is True iff no disable value is present. -# `bool(_env_values & _enable_values)` is True iff any enable value is present. -TELEMETRY_ENABLED = not _env_values & _disable_values and ( - bool(_env_values & _enable_values) or "dev" not in QSHARP_VERSION -) - -BATCH_INTERVAL_SEC = int(os.environ.get("QSHARP_PYTHON_TELEMETRY_INTERVAL") or 60) - - -# The below is taken from the Azure Monitor Python SDK -def _getlocale() -> str: - try: - with warnings.catch_warnings(): - # Workaround for https://github.com/python/cpython/issues/82986 by continuing to use getdefaultlocale() even though it has been deprecated. - # Ignore the deprecation warnings to reduce noise - warnings.simplefilter("ignore", category=DeprecationWarning) - return locale.getdefaultlocale()[0] or "" - except AttributeError: - # Use this as a fallback if locale.getdefaultlocale() doesn't exist (>Py3.13) - return locale.getlocale()[0] or "" - - -# Minimal device information to include with telemetry -AI_DEVICE_LOCALE = _getlocale() -AI_DEVICE_OS_VERSION = platform.version() - - -class Metric(TypedDict): - """Used internally for objects in the telemetry queue""" - - name: str - value: float - count: int - properties: Dict[str, Any] - type: str - - -class PendingMetric(Metric): - """Used internally to aggregate metrics before sending""" - - min: float - max: float - - -# Maintain a collection of custom metrics to log, stored by metric name with a list entry -# for each unique set of properties per metric name -pending_metrics: Dict[str, List[PendingMetric]] = {} - -# The telemetry queue is used to send telemetry from the main thread to the telemetry thread -# This simplifies any thread-safety concerns, and avoids the need for locks, etc. -telemetry_queue: Any = SimpleQueue() # type 'Any' until we get off Python 3.8 builds - - -def log_telemetry( - name: str, - value: float, - count: int = 1, - properties: Dict[str, Any] = {}, - type: Literal["counter", "histogram"] = "counter", -) -> None: - """ - Logs a custom metric with the name provided. Properties are optional and can be used to - capture additional context about the metric (but should be a relatively static set of values, as - each unique set of properties will be sent as a separate metric and creates a separate 'dimension' - in the backend telemetry store). - - The type can be either 'counter' or 'histogram'. A 'counter' is a simple value that is summed - over time, such as how many times an event occurs, while a 'histogram' is used to track 'quantative' - values, such as the distribution of values over time, e.g., the duration of an operation. - - Example usage for a counter: - - log_telemetry("qir_generated", 1, properties={"profile": "base", "qsharp.version": "1.9.0"}) - - Example usage for a histogram: - - log_telemetry("simulation_duration", 123.45, type="histogram") - - """ - if not TELEMETRY_ENABLED: - return - - obj: Metric = { - "name": name, - "value": value, - "count": count, - "properties": {**properties, "qsharp.version": QSHARP_VERSION}, - "type": type, - } - - logger.debug("Queuing telemetry: %s", obj) - telemetry_queue.put(obj) - - -def _add_to_pending(metric: Metric): - """Used by the telemetry thread to aggregate metrics before sending""" - - if metric["type"] not in ["counter", "histogram"]: - raise Exception("Metric must be of type counter or histogram") - - # Get or create the entry list for this name - name_entries = pending_metrics.setdefault(metric["name"], []) - - # Try to find the entry with matching properties - # This relies on the fact dicts with matching keys/values compare equal in Python - prop_entry = next( - ( - entry - for entry in name_entries - if entry["properties"] == metric["properties"] - ), - None, - ) - if prop_entry is None: - new_entry: PendingMetric = { - **metric, - "min": metric["value"], - "max": metric["value"], - } - name_entries.append(new_entry) - else: - if prop_entry["type"] != metric["type"]: - raise Exception("Cannot mix counter and histogram for the same metric name") - prop_entry["value"] += metric["value"] - prop_entry["count"] += metric["count"] - prop_entry["min"] = min(prop_entry["min"], metric["value"]) - prop_entry["max"] = max(prop_entry["max"], metric["value"]) - - -def _pending_to_payload() -> List[Dict[str, Any]]: - """Converts the pending metrics to the JSON payload for Azure Monitor""" - - result_array: List[Dict[str, Any]] = [] - formatted_time = ( - datetime.now(timezone.utc) - .isoformat(timespec="microseconds") - .replace("+00:00", "Z") - ) - for name in pending_metrics: - for unique_props in pending_metrics[name]: - # The below matches the entry format for Azure Monitor REST API - entry: Dict[str, Any] = { - "ver": 1, - "name": "Microsoft.ApplicationInsights.Metric", - "time": formatted_time, - "sampleRate": 100.0, - "iKey": AIKEY, - "tags": { - "ai.device.locale": AI_DEVICE_LOCALE, - "ai.device.osVersion": AI_DEVICE_OS_VERSION, - }, - "data": { - "baseType": "MetricData", - "baseData": { - "ver": 2, - "metrics": [ - { - "name": unique_props["name"], - "value": unique_props["value"], - "count": unique_props["count"], - } - ], - "properties": unique_props["properties"], - }, - }, - } - # Histogram values differ only in that they have min/max values also - if unique_props["type"] == "histogram": - entry["data"]["baseData"]["metrics"][0]["min"] = unique_props["min"] - entry["data"]["baseData"]["metrics"][0]["max"] = unique_props["max"] - - result_array.append(entry) - - return result_array - - -def _post_telemetry() -> bool: - """Posts the pending telemetry to Azure Monitor""" - - if len(pending_metrics) == 0: - return True - - payload = json.dumps(_pending_to_payload()).encode("utf-8") - logger.debug("Sending telemetry request: %s", payload) - try: - request = urllib.request.Request(AIURL, data=payload, method="POST") - request.add_header("Content-Type", "application/json") - with urllib.request.urlopen(request, timeout=10) as response: - logger.debug("Telemetry response: %s", response.status) - # On a successful post, clear the pending list. (Else they will be included on the next retry) - pending_metrics.clear() - return True - - except Exception: - logger.debug( - "Failed to post telemetry. Pending metrics will be retried at the next interval." - ) - return False - - -# This is the thread that aggregates and posts telemetry at a regular interval. -# The main thread will signal the thread loop to exit when the process is about to exit. -def _telemetry_thread_start(): - next_post_sec: Union[float, None] = None - - def on_metric(msg: Metric): - nonlocal next_post_sec - - # Add to the pending batch to send next - _add_to_pending(msg) - - # Schedule the next post if we don't have one scheduled - if next_post_sec == None: - next_post_sec = time.monotonic() + BATCH_INTERVAL_SEC - - while True: - try: - # Block if no timeout, else wait a maximum of time until the next post is due - timeout: Union[float, None] = None - if next_post_sec: - timeout = max(next_post_sec - time.monotonic(), 0) - msg = telemetry_queue.get(timeout=timeout) - - if msg == "exit": - logger.debug("Exiting telemetry thread") - if not _post_telemetry(): - logger.debug("Failed to post telemetry on exit") - return - else: - on_metric(msg) - # Loop until the queue has been drained. This will cause the 'Empty' exception - # below once the queue is empty and it's time to post - continue - except Empty: - # No more telemetry within timeout, so write what we have pending - _ = _post_telemetry() - - # If we get here, it's after a post attempt. Pending will still have items if the attempt - # failed, so updated the time for the next attempt in that case. - if len(pending_metrics) == 0: - next_post_sec = None - else: - next_post_sec = time.monotonic() + BATCH_INTERVAL_SEC - - -# When the process is about to exit, notify the telemetry thread to flush, and wait max 3 sec before exiting anyway -def _on_exit(): - logger.debug("In on_exit handler") - telemetry_queue.put("exit") - # Wait at most 3 seconds for the telemetry thread to flush and exit - telemetry_thread.join(timeout=3) - - -# Mark the telemetry thread as a daemon thread, else it will keep the process alive when the main thread exits -if TELEMETRY_ENABLED: - telemetry_thread = Thread(target=_telemetry_thread_start, daemon=True) - telemetry_thread.start() - atexit.register(_on_exit) +# Deprecated: use qdk.telemetry instead. +from qdk.telemetry import * # noqa: F401,F403 diff --git a/source/pip/qsharp/telemetry_events.py b/source/pip/qsharp/telemetry_events.py index edffb17585..86dd1c96ce 100644 --- a/source/pip/qsharp/telemetry_events.py +++ b/source/pip/qsharp/telemetry_events.py @@ -1,357 +1,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from .telemetry import log_telemetry -import math -from typing import Union - -# For metrics such as duration, we want to capture things like how many shots or qubits in -# the additional properties. However properties shouldn't be 'continuous' values, as they -# create new 'dimensions' on the backend, which is limited, thus we want to bucket these properties. - -# See some of the notes at: https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/metrics-custom-overview#design-limitations-and-considerations - - -def get_next_power_of_ten_bucket(value: int) -> int: - if value <= 1: - return 1 - elif value >= 1000000: - # Limit the buckets upper bound - return 1000000 - else: - # Bucket into nearest (rounded up) power of 10, e.g. 75 -> 100, 450 -> 1000, etc. - return 10 ** math.ceil(math.log10(value)) - - -# gets the order of magnitude for the number of qubits -def get_qubits_bucket(qubits: Union[str, int]) -> str: - if qubits == "unknown": - return "unknown" - qubits = int(qubits) - if qubits <= 1: - return "1" - elif qubits >= 50: - return "50" - else: - # integer divide by 5 to get nearest 5 - return str(qubits // 5 * 5) - - -def on_import() -> None: - log_telemetry("qsharp.import", 1) - - -def on_qdk_import() -> None: - log_telemetry("qdk.import", 1) - - -def on_run(shots: int, noise: bool, qubit_loss: bool) -> None: - log_telemetry( - "qsharp.run", - 1, - properties={ - "shots": get_next_power_of_ten_bucket(shots), - "noise": noise, - "qubit_loss": qubit_loss, - }, - ) - - -def on_run_end(durationMs: float, shots: int) -> None: - log_telemetry( - "qsharp.run.durationMs", - durationMs, - properties={"shots": get_next_power_of_ten_bucket(shots)}, - type="histogram", - ) - - -def on_run_qasm(shots: int, noise: bool, qubit_loss: bool) -> None: - log_telemetry( - "qsharp.run_qasm", - 1, - properties={ - "shots": get_next_power_of_ten_bucket(shots), - "noise": noise, - "qubit_loss": qubit_loss, - }, - ) - - -def on_run_qasm_end(durationMs: float, shots: int) -> None: - log_telemetry( - "qsharp.run_qasm.durationMs", - durationMs, - properties={"shots": get_next_power_of_ten_bucket(shots)}, - type="histogram", - ) - - -def on_eval() -> None: - log_telemetry( - "qsharp.eval", - 1, - ) - - -def on_eval_end(durationMs: float) -> None: - log_telemetry( - "qsharp.eval.durationMs", - durationMs, - type="histogram", - ) - - -def on_import_qasm() -> None: - log_telemetry( - "qsharp.import_qasm", - 1, - ) - - -def on_import_qasm_end(durationMs: float) -> None: - log_telemetry( - "qsharp.import_qasm.durationMs", - durationMs, - type="histogram", - ) - - -def on_run_cell() -> None: - log_telemetry( - "qsharp.run.cell", - 1, - ) - - -def on_run_cell_end(durationMs: float) -> None: - log_telemetry( - "qsharp.run.cell.durationMs", - durationMs, - type="histogram", - ) - - -def on_compile(profile: str) -> None: - log_telemetry("qsharp.compile", 1, properties={"profile": profile}) - - -def on_compile_end(durationMs: float, profile: str) -> None: - log_telemetry( - "qsharp.compile.durationMs", - durationMs, - properties={"profile": profile}, - type="histogram", - ) - - -def on_compile_qasm(profile: str) -> None: - log_telemetry("qsharp.compile_qasm", 1, properties={"profile": profile}) - - -def on_compile_qasm_end(durationMs: float, profile: str) -> None: - log_telemetry( - "qsharp.compile_qasm.durationMs", - durationMs, - properties={"profile": profile}, - type="histogram", - ) - - -def on_estimate() -> None: - log_telemetry( - "qsharp.estimate", - 1, - ) - - -def on_estimate_end(durationMs: float, qubits: Union[str, int]) -> None: - log_telemetry( - "qsharp.estimate.durationMs", - durationMs, - properties={"qubits": get_qubits_bucket(qubits)}, - type="histogram", - ) - - -def on_estimate_qasm() -> None: - log_telemetry( - "qsharp.estimate_qasm", - 1, - ) - - -def on_estimate_qasm_end(durationMs: float, qubits: Union[str, int]) -> None: - log_telemetry( - "qsharp.estimate_qasm.durationMs", - durationMs, - properties={"qubits": get_qubits_bucket(qubits)}, - type="histogram", - ) - - -def on_circuit() -> None: - log_telemetry( - "qsharp.circuit", - 1, - ) - - -def on_circuit_end(durationMs: float) -> None: - log_telemetry( - "qsharp.circuit.durationMs", - durationMs, - type="histogram", - ) - - -def on_circuit_qasm() -> None: - log_telemetry( - "qsharp.circuit_qasm", - 1, - ) - - -def on_circuit_qasm_end(durationMs: float) -> None: - log_telemetry( - "qsharp.circuit_qasm.durationMs", - durationMs, - type="histogram", - ) - - -# Qiskit telemetry events - - -def on_qiskit_run(shots: int, num_circuits: int) -> None: - log_telemetry( - "qiskit.run", - 1, - properties={ - "shots": get_next_power_of_ten_bucket(shots), - "circuits": get_next_power_of_ten_bucket(num_circuits), - }, - ) - - -def on_qiskit_run_end(shots: int, num_circuits: int, duration_ms: float) -> None: - log_telemetry( - "qiskit.run.durationMs", - duration_ms, - properties={ - "shots": get_next_power_of_ten_bucket(shots), - "circuits": get_next_power_of_ten_bucket(num_circuits), - }, - type="histogram", - ) - - -def on_qiskit_run_re() -> None: - log_telemetry( - "qiskit.run.re", - 1, - ) - - -def on_qiskit_run_re_end(duration_ms: float) -> None: - log_telemetry( - "qiskit.run.re.durationMs", - duration_ms, - type="histogram", - ) - - -def on_neutral_atom_init(default_layout: bool) -> None: - log_telemetry( - "neutral_atom.device.init", - 1, - properties={"default_layout": default_layout}, - ) - - -def on_neutral_atom_compile() -> None: - log_telemetry( - "neutral_atom.device.compile", - 1, - ) - - -def on_neutral_atom_compile_end(duration_ms: float) -> None: - log_telemetry( - "neutral_atom.device.compile.durationMs", - duration_ms, - type="histogram", - ) - - -def on_neutral_atom_trace() -> None: - log_telemetry( - "neutral_atom.device.trace", - 1, - ) - - -def on_neutral_atom_trace_end(duration_ms: float) -> None: - log_telemetry( - "neutral_atom.device.trace.durationMs", - duration_ms, - type="histogram", - ) - - -def on_neutral_atom_cpu_fallback() -> None: - log_telemetry( - "neutral_atom.device.cpu_fallback", - 1, - ) - - -def on_neutral_atom_simulate(shots: int, noise: bool, type: str) -> None: - log_telemetry( - "neutral_atom.device.simulate", - 1, - properties={ - "shots": get_next_power_of_ten_bucket(shots), - "noise": noise, - "type": type, - }, - ) - - -def on_neutral_atom_simulate_end( - duration_ms: float, shots: int, noise: bool, type: str -) -> None: - log_telemetry( - "neutral_atom.device.simulate.durationMs", - duration_ms, - properties={ - "shots": get_next_power_of_ten_bucket(shots), - "noise": noise, - "type": type, - }, - type="histogram", - ) - - -# QRE telemetry events - - -def on_qre_estimate(post_process: bool, use_graph: bool) -> None: - log_telemetry( - "qsharp.qre.estimate", - 1, - properties={ - "post_process": post_process, - "use_graph": use_graph, - }, - ) - - -def on_qre_application_created(application_type: str) -> None: - log_telemetry( - "qsharp.qre.application.created", - 1, - properties={ - "application_type": application_type, - }, - ) +# Deprecated: use qdk.telemetry_events instead. +from qdk.telemetry_events import * # noqa: F401,F403 diff --git a/source/pip/qsharp/utils/__init__.py b/source/pip/qsharp/utils/__init__.py index 03d71482e7..618a84d247 100644 --- a/source/pip/qsharp/utils/__init__.py +++ b/source/pip/qsharp/utils/__init__.py @@ -1,7 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from ._utils import dump_operation +# Deprecated: use qdk.qsharp.dump_operation instead. +from qdk.qsharp import dump_operation # noqa: F401 __all__ = [ "dump_operation", diff --git a/source/pip/qsharp/utils/_utils.py b/source/pip/qsharp/utils/_utils.py deleted file mode 100644 index 26984dc4db..0000000000 --- a/source/pip/qsharp/utils/_utils.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -from .._qsharp import run -from typing import List -import math - - -def dump_operation(operation: str, num_qubits: int) -> List[List[complex]]: - """ - Returns a square matrix of complex numbers representing the operation performed. - - :param operation: The operation to be performed, which must operate on a list of qubits. - :param num_qubits: The number of qubits to be used. - - :return: The matrix representing the operation. - :rtype: List[List[complex]] - """ - code = f"""{{ - let op = {operation}; - use (targets, extra) = (Qubit[{num_qubits}], Qubit[{num_qubits}]); - for i in 0..{num_qubits}-1 {{ - H(targets[i]); - CNOT(targets[i], extra[i]); - }} - operation ApplyOp (op : (Qubit[] => Unit), targets : Qubit[]) : Unit {{ op(targets); }} - ApplyOp(op, targets); - Microsoft.Quantum.Diagnostics.DumpMachine(); - ResetAll(targets + extra); - }}""" - result = run(code, shots=1, save_events=True)[0] - state = result["events"][-1].state_dump().get_dict() - num_entries = pow(2, num_qubits) - factor = math.sqrt(num_entries) - ndigits = 6 - matrix = [] - for i in range(num_entries): - matrix += [[]] - for j in range(num_entries): - entry = state.get(i * num_entries + j) - if entry is None: - matrix[i] += [complex(0, 0)] - else: - matrix[i] += [ - complex( - round(factor * entry.real, ndigits), - round(factor * entry.imag, ndigits), - ) - ] - return matrix diff --git a/source/pip/tests-integration/conftest.py b/source/pip/tests-integration/conftest.py deleted file mode 100644 index 3635045928..0000000000 --- a/source/pip/tests-integration/conftest.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -""" -This file is used to configure pytest for the test suite. - -- It attempts to import necessary modules from test_circuits. - -Fixtures and other configurations for pytest can be added to this file to -be shared across multiple test files. -""" - -from interop_qiskit.test_circuits import * diff --git a/source/qdk_package/.gitignore b/source/qdk_package/.gitignore index 7a560a45da..6b5bae3b7c 100644 --- a/source/qdk_package/.gitignore +++ b/source/qdk_package/.gitignore @@ -1,4 +1,3 @@ /build/ -/src/ /*.egg-info/ /doc/ diff --git a/source/qdk_package/API_SURFACE.md b/source/qdk_package/API_SURFACE.md new file mode 100644 index 0000000000..58c24d3182 --- /dev/null +++ b/source/qdk_package/API_SURFACE.md @@ -0,0 +1,345 @@ +# `qdk` Package — Public API Surface + +> **Delete this file before merging.** + +## `qdk` + +``` +qdk +├── code # submodule — dynamic Q# callable namespace +├── Result # enum (Zero, One, Loss) +├── TargetProfile # enum (Base, Adaptive_RI, Adaptive_RIF, Adaptive_RIFLA, Unrestricted) +├── StateDump # class — quantum state snapshot +├── ShotResult # TypedDict — single shot result +├── PauliNoise # class — (x, y, z) noise tuple +├── DepolarizingNoise # class — uniform Pauli noise +├── BitFlipNoise # class — X-only noise +├── PhaseFlipNoise # class — Z-only noise +├── init() # function — initialize the Q# interpreter +├── set_quantum_seed() # function +├── set_classical_seed() # function +└── dump_machine() # function — get current state +``` + +## `qdk.qsharp` + +Q# interpreter — the main entry point for writing and running Q# programs. + +``` +qdk.qsharp +│ +│ # Interpreter lifecycle +├── init() # initialize interpreter with target profile, project root, etc. +├── get_interpreter() # get the current Interpreter instance +├── get_config() # get the current Config +│ +│ # Execution +├── eval() # evaluate Q# source code +├── run() # run an entry expression for N shots +├── compile() # compile to QIR → QirInputData +├── circuit() # generate a circuit diagram → Circuit +├── estimate() # resource estimation (deprecated — use qdk.qre) +├── logical_counts() # get logical resource counts → LogicalCounts +│ +│ # State inspection +├── dump_machine() # get current quantum state → StateDump +├── dump_circuit() # get circuit so far → Circuit +├── dump_operation() # get unitary matrix of an operation +├── set_quantum_seed() # set quantum RNG seed +├── set_classical_seed() # set classical RNG seed +│ +│ # Types & Data Classes +├── Config # class — interpreter configuration +├── QirInputData # class — compiled QIR output +├── StateDump # class — quantum state data +├── ShotResult # TypedDict — shot result with events/messages +│ +│ # Noise types +├── PauliNoise # class — (x, y, z) noise specification +├── DepolarizingNoise # class — uniform depolarizing +├── BitFlipNoise # class — bit-flip noise +├── PhaseFlipNoise # class — phase-flip noise +├── NoiseConfig # class — per-gate noise configuration +│ +│ # Enums +├── Result # enum (Zero, One, Loss) +├── Pauli # enum (I, X, Y, Z) +├── TargetProfile # enum (Base, Adaptive_RI, ..., Unrestricted) +├── CircuitGenerationMethod # enum (ClassicalEval, Simulate, Static) +│ +│ # Native types +├── Interpreter # class — the Q# interpreter +├── Circuit # class — circuit representation +├── CircuitConfig # class — circuit generation options +├── Output # class — interpreter output +├── GlobalCallable # class — Q# callable reference +├── Closure # class — Q# closure reference +├── StateDumpData # class — raw state dump from native +├── QSharpError # exception +│ +│ # Estimator types (re-exported) +├── EstimatorResult # class — resource estimation result +├── EstimatorParams # class — resource estimation parameters +└── LogicalCounts # class — logical resource counts +``` + +## `qdk.simulation` + +Simulation APIs — neutral atom device, QIR execution, and noisy simulators. + +``` +qdk.simulation +├── NeutralAtomDevice # class — neutral atom device compiler & simulator +│ ├── compile(program, verbose) +│ ├── show_trace(qir) +│ └── simulate(qir, shots, noise, type, seed) +├── NoiseConfig # class — per-gate noise tables +│ ├── .x, .y, .z, .h, .s, .t, ... # NoiseTable per gate type +│ ├── .intrinsics # NoiseIntrinsicsTable +│ ├── intrinsic(name, num_qubits) +│ └── load_csv_dir(dir_path) +├── run_qir() # function — run QIR with optional noise +│ +│ # Experimental noisy simulation +├── NoisySimulatorError # exception +├── Operation # class — Kraus operator representation +├── Instrument # class — quantum instrument +├── DensityMatrixSimulator # class — density matrix simulator +├── StateVectorSimulator # class — state vector simulator +├── DensityMatrix # class — density matrix state +└── StateVector # class — state vector state +``` + +## `qdk.estimator` + +Resource estimation (v1) — physical qubit and QEC parameter estimation. + +``` +qdk.estimator +├── EstimatorParams # class — estimation input parameters +├── EstimatorInputParamsItem # class — single parameter set +├── EstimatorResult # class — estimation output (dict subclass) +│ ├── data(idx) +│ ├── summary, diagram, plot() +│ └── summary_data_frame() +├── LogicalCounts # class — logical resource counts (dict subclass) +│ └── estimate(params) → EstimatorResult +├── EstimatorError # exception +│ +│ # Parameter building blocks +├── QubitParams # class — predefined qubit models +│ └── GATE_US_E3, GATE_US_E4, GATE_NS_E3, GATE_NS_E4, MAJ_NS_E4, MAJ_NS_E6 +├── QECScheme # class — predefined QEC schemes +│ └── SURFACE_CODE, FLOQUET_CODE +├── MeasurementErrorRate # dataclass +├── EstimatorQubitParams # dataclass +├── EstimatorQecScheme # dataclass +├── ProtocolSpecificDistillationUnitSpecification # dataclass +├── DistillationUnitSpecification # dataclass +├── ErrorBudgetPartition # dataclass +└── EstimatorConstraints # dataclass +``` + +## `qdk.openqasm` + +OpenQASM 3.0 compilation and execution. + +``` +qdk.openqasm +├── run() # function — run OpenQASM program +├── compile() # function — compile to QIR +├── circuit() # function — generate circuit diagram +├── estimate() # function — resource estimation (deprecated) +├── import_openqasm() # function — import OpenQASM into interpreter +├── ProgramType # enum (File, Operation, Fragments) +├── OutputSemantics # enum (Qiskit, OpenQasm, ResourceEstimation) +└── QasmError # exception +``` + +## `qdk.qiskit` + +Qiskit interop — backends, jobs, and resource estimation. + +``` +qdk.qiskit +├── QSharpBackend # class — Qiskit BackendV2 for Q# simulation +├── NeutralAtomBackend # class — Qiskit BackendV2 for neutral atom +├── ResourceEstimatorBackend # class — Qiskit BackendV2 for resource estimation +├── QirTarget # class — Qiskit Target helper +├── estimate() # function — estimate a QuantumCircuit +├── EstimatorParams # class (re-exported from qdk.estimator) +├── EstimatorResult # class (re-exported from qdk.estimator) +├── QasmError # exception +│ +│ # Jobs +├── QsJob # class — abstract job base +├── QsSimJob # class — simulation job +├── ReJob # class — resource estimation job +├── QsJobSet # class — multi-circuit job set +│ +│ # Submodules +├── backends/ # backend implementations +│ ├── Compilation, Errors +│ ├── NeutralAtomTarget +│ └── RemoveDelays (pass) +├── jobs/ # job implementations +├── execution/ # execution helpers +└── passes/ # transpiler passes +``` + +## `qdk.cirq` + +Cirq interop — neutral atom sampler. + +``` +qdk.cirq +├── NeutralAtomSampler # class — cirq.Sampler for neutral atom simulation +│ └── run_sweep(program, params, repetitions) +└── NeutralAtomCirqResult # class — cirq.ResultDict with raw shot access +``` + +## `qdk.qre` + +Quantum Resource Estimation v3. + +``` +qdk.qre +│ +│ # Top-level functions +├── estimate() # function — run full estimation pipeline +├── constraint() # function — create an ISA constraint +├── plot_estimates() # function — visualize estimation results +├── instruction_name() # function — ID → name lookup +├── property_name() # function — ID → name lookup +├── property_name_to_key() # function — name → ID lookup +│ +│ # Function builders (for ISA properties) +├── block_linear_function() # function +├── constant_function() # function +├── linear_function() # function +├── generic_function() # function +│ +│ # Core types (from Rust) +├── ISA # class — instruction set architecture +├── ISARequirements # class — ISA constraint set +├── Instruction # class — single instruction definition +├── InstructionFrontier # class — Pareto frontier of instructions +├── Constraint # class — ISA constraint +├── ConstraintBound # class — comparison bound (lt, le, eq, gt, ge) +├── EstimationResult # class — single estimation result +├── FactoryResult # class — factory estimation result +├── Trace # class — algorithm execution trace +├── Block # class — trace block +│ +│ # Python framework types +├── Application # abstract class — algorithm definition +├── Architecture # abstract class — hardware model +├── ISAContext # class — enumeration context +├── ISATransform # abstract class — ISA transformation +├── ISAQuery # abstract class — ISA enumeration query +├── ISARefNode # class — enumeration leaf node +├── ISA_ROOT # constant — root enumeration node +├── TraceQuery # class — trace enumeration query +├── TraceTransform # abstract class — trace transformation +├── PSSPC # dataclass — Pauli-based rotation synthesis +├── LatticeSurgery # dataclass — lattice surgery transform +├── Encoding # IntEnum (PHYSICAL=0, LOGICAL=1) +├── LOGICAL # constant +├── PHYSICAL # constant +├── InstructionSource # class — instruction provenance +│ +│ # Result types +├── EstimationTable # class — tabular estimation results +├── EstimationTableEntry # frozen dataclass — single result row +├── EstimationTableColumn # frozen dataclass — column definition +│ +│ # Submodules +├── instruction_ids # module — integer constants (PAULI_X, H, CNOT, T, ...) +├── property_keys # module — integer constants (DISTANCE, RUNTIME, ...) +│ +├── application/ # application definitions +│ ├── CirqApplication # dataclass +│ ├── QIRApplication # dataclass +│ ├── QSharpApplication # dataclass +│ └── OpenQASMApplication # dataclass +│ +├── interop/ # trace builders +│ ├── trace_from_cirq() # function +│ ├── trace_from_entry_expr() # function +│ ├── trace_from_entry_expr_cached() # function +│ ├── trace_from_qir() # function +│ ├── PushBlock, PopBlock # classes — Cirq custom gates +│ ├── QubitType, TypedQubit # classes — typed qubits +│ ├── PeakUsageGreedyQubitManager # class — qubit manager +│ ├── ReadFromMemoryGate # class +│ ├── WriteToMemoryGate # class +│ ├── write_to_memory() # function +│ ├── read_from_memory() # function +│ └── assert_qubits_type() # function +│ +└── models/ # hardware models + ├── GateBased # class — gate-based qubit architecture + ├── Majorana # class — Majorana qubit architecture + ├── SurfaceCode # class — surface code QEC + ├── ThreeAux # class — 3-auxiliary QEC + ├── OneDimensionalYokedSurfaceCode # class — yoked surface code (1D) + ├── TwoDimensionalYokedSurfaceCode # class — yoked surface code (2D) + ├── Litinski19Factory # class — magic state factory + ├── MagicUpToClifford # class — factory utility + └── RoundBasedFactory # class — round-based factory +``` + +## `qdk.applications` + +Domain-specific quantum applications. + +``` +qdk.applications +└── magnets/ # quantum magnetism + │ + │ # Geometry + ├── CompleteBipartiteGraph # class + ├── CompleteGraph # class + ├── Chain1D # class + ├── Ring1D # class + ├── Patch2D # class + ├── Torus2D # class + │ + │ # Models + ├── Model # class — base model + ├── IsingModel # class + ├── HeisenbergModel # class + │ + │ # Trotter + ├── TrotterStep # class + ├── TrotterExpansion # class + ├── strang_splitting() # function + ├── suzuki_recursion() # function + ├── yoshida_recursion() # function + ├── fourth_order_trotter_suzuki() # function + │ + │ # Utilities + ├── Hyperedge # class + ├── Hypergraph # class + ├── HypergraphEdgeColoring # class + ├── Pauli # class + ├── PauliString # class + ├── PauliX # constant + ├── PauliY # constant + └── PauliZ # constant +``` + +## `qdk.azure` + +Azure Quantum integration (requires `pip install qdk[azure]`). +Re-exports `azure.quantum.*`. + +## `qdk.widgets` + +Jupyter widgets (requires `pip install qdk[jupyter]`). +Re-exports `qsharp_widgets.*`. + +## `qdk.code` + +Dynamic namespace populated at runtime by the Q# interpreter. +Q# callables and types become attributes (e.g., `qdk.code.Microsoft.Quantum.*`). diff --git a/source/pip/Cargo.toml b/source/qdk_package/Cargo.toml similarity index 95% rename from source/pip/Cargo.toml rename to source/qdk_package/Cargo.toml index 7d576bbcc9..9a36669c4c 100644 --- a/source/pip/Cargo.toml +++ b/source/qdk_package/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "qsharp" -description = "Q# Python Bindings" +name = "qdk" +description = "QDK Python Bindings" version.workspace = true authors.workspace = true @@ -59,4 +59,4 @@ crate-type = ["cdylib"] doctest = false [package.metadata.maturin] -name = "qsharp._native" +name = "qdk._native" diff --git a/source/qdk_package/LICENSE.txt b/source/qdk_package/LICENSE.txt new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/source/qdk_package/LICENSE.txt @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/source/pip/MANIFEST.in b/source/qdk_package/MANIFEST.in similarity index 100% rename from source/pip/MANIFEST.in rename to source/qdk_package/MANIFEST.in diff --git a/source/qdk_package/README.md b/source/qdk_package/README.md index 7ffda46294..678e9dcc1b 100644 --- a/source/qdk_package/README.md +++ b/source/qdk_package/README.md @@ -28,7 +28,7 @@ For Qiskit integration, which exposes Qiskit interop utilities in the `qdk.qiski pip install "qdk[qiskit]" ``` -For Cirq integration, which exposes Cirq interop utilities in the `qdk.azure.cirq` submodule: +For Cirq integration, which exposes Cirq interop utilities in the `qdk.cirq` submodule: ```bash pip install "qdk[cirq]" @@ -70,12 +70,17 @@ Histogram(results) Submodules: -- `qdk.qsharp` – exports the same APIs as the `qsharp` Python package -- `qdk.openqasm` – exports the same APIs as the `openqasm` submodule of the `qsharp` Python package. -- `qdk.estimator` – exports the same APIs as the `estimator` submodule of the `qsharp` Python package. -- `qdk.widgets` – exports the Jupyter widgets available from the `qsharp-widgets` Python package (requires the `qdk[jupyter]` extra to be installed). -- `qdk.azure` – exports the Python APIs available from the `azure-quantum` Python package (requires the `qdk[azure]` extra to be installed). -- `qdk.qiskit` – exports the same APIs as the `interop.qiskit` submodule of the `qsharp` Python package (requires the `qdk[qiskit]` extra to be installed). +- `qdk.qsharp` – Q# interpreter functions: `init`, `eval`, `run`, `compile`, `circuit`, `estimate`, and related types. +- `qdk.openqasm` – OpenQASM compilation and execution. +- `qdk.estimator` – resource estimation utilities. +- `qdk.simulation` – noise-aware simulation utilities: `NeutralAtomDevice`, `NoiseConfig`, `run_qir`, `DensityMatrixSimulator`, `StateVectorSimulator`, and related types. +- `qdk.code` – dynamic namespace populated at runtime with user-defined Q# and OpenQASM callables. +- `qdk.qre` – quantum resource estimation v3: `estimate`, `Application`, `Architecture`, `ISA`, `ISATransform`, and related types. +- `qdk.applications` – domain-specific quantum applications (e.g. `qdk.applications.magnets`). +- `qdk.widgets` – Jupyter widgets for visualization (requires the `qdk[jupyter]` extra). +- `qdk.azure` – Azure Quantum service integration (requires the `qdk[azure]` extra). +- `qdk.qiskit` – Qiskit interop: `QSharpBackend`, `NeutralAtomBackend`, and related types (requires the `qdk[qiskit]` extra). +- `qdk.cirq` – Cirq interop: `NeutralAtomSampler` (requires the `qdk[cirq]` extra). ### Top level exports @@ -100,4 +105,14 @@ For convenience, the following helpers and types are also importable directly fr ## Telemetry This library sends telemetry. Minimal anonymous data is collected to help measure feature usage and performance. -All telemetry events can be seen in the source file [telemetry_events.py](https://github.com/microsoft/qdk/tree/main/source/pip/qsharp/telemetry_events.py). +All telemetry events can be seen in the source file [telemetry_events.py](https://github.com/microsoft/qdk/tree/main/source/qdk_package/qdk/telemetry_events.py). + +To disable sending telemetry from this package, set the environment variable `QDK_PYTHON_TELEMETRY=none` + +## Support + +For more information about the Microsoft Quantum Development Kit, visit [https://aka.ms/qdk](https://aka.ms/qdk). + +## Contributing + +Q# welcomes your contributions! Visit the Q# GitHub repository at [https://github.com/microsoft/qdk] to find out more about the project. diff --git a/source/pip/benchmarks/bench_qre.py b/source/qdk_package/benchmarks/bench_qre.py similarity index 92% rename from source/pip/benchmarks/bench_qre.py rename to source/qdk_package/benchmarks/bench_qre.py index 6697aba2ad..1ded85ffa9 100644 --- a/source/pip/benchmarks/bench_qre.py +++ b/source/qdk_package/benchmarks/bench_qre.py @@ -3,15 +3,15 @@ import timeit from dataclasses import dataclass, KW_ONLY, field -from qsharp.qre import linear_function, generic_function -from qsharp.qre._architecture import _make_instruction -from qsharp.qre.models import ( +from qdk.qre import linear_function, generic_function +from qdk.qre._architecture import _make_instruction +from qdk.qre.models import ( GateBased, SurfaceCode, TwoDimensionalYokedSurfaceCode, Litinski19Factory, ) -from qsharp.qre._enumeration import _enumerate_instances +from qdk.qre._enumeration import _enumerate_instances def bench_enumerate_instances(): diff --git a/source/qdk_package/pyproject.toml b/source/qdk_package/pyproject.toml index edb2e19ec2..6d7ebed0cc 100644 --- a/source/qdk_package/pyproject.toml +++ b/source/qdk_package/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools>=64", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["maturin ~= 1.10.2"] +build-backend = "maturin" [project] name = "qdk" @@ -10,7 +10,22 @@ readme = "README.md" authors = [ { name = "Microsoft" } ] license = { file = "LICENSE.txt" } requires-python = ">=3.10" -dependencies = ["qsharp==0.0.0", "pyqir>=0.12.3,<0.13"] +dependencies = ["pyqir>=0.12.3,<0.13"] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python", + "Programming Language :: Rust", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", +] [project.optional-dependencies] jupyter = ["qsharp-widgets==0.0.0", "qsharp-jupyterlab==0.0.0"] @@ -30,6 +45,9 @@ all = [ "qsharp-jupyterlab==0.0.0", ] -[tool.setuptools.packages.find] -where = ["."] +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.maturin] +module-name = "qdk._native" diff --git a/source/qdk_package/qdk/__init__.py b/source/qdk_package/qdk/__init__.py index dfe5b0d012..e6e3e6d87e 100644 --- a/source/qdk_package/qdk/__init__.py +++ b/source/qdk_package/qdk/__init__.py @@ -15,20 +15,14 @@ """ - -from qsharp.telemetry_events import on_qdk_import +from .telemetry_events import on_qdk_import on_qdk_import() # Some common utilities are lifted to the qdk root. -from qsharp import code -from qsharp import ( - set_quantum_seed, - set_classical_seed, - dump_machine, - init, - Result, - TargetProfile, +from . import code +from ._native import Result, TargetProfile +from ._types import ( StateDump, ShotResult, PauliNoise, @@ -36,6 +30,21 @@ BitFlipNoise, PhaseFlipNoise, ) +from ._interpreter import ( + set_quantum_seed, + set_classical_seed, + dump_machine, + init, +) + +# Register the %%qsharp cell magic when running inside IPython/Jupyter. +try: + if __IPYTHON__: # type: ignore + from ._ipython import register_magic + + register_magic() +except NameError: + pass # utilities lifted from qsharp __all__ = [ diff --git a/source/qdk_package/qdk/_adaptive_bytecode.py b/source/qdk_package/qdk/_adaptive_bytecode.py new file mode 100644 index 0000000000..aa244fc59c --- /dev/null +++ b/source/qdk_package/qdk/_adaptive_bytecode.py @@ -0,0 +1,130 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Shared opcode constants for the Adaptive Profile QIR bytecode interpreter. + +These constants define the bytecode encoding used by the Python AdaptiveProfilePass +(emitter) and the Rust GPU receiver. Values must stay in sync with the Rust +``bytecode.rs`` module and the WGSL interpreter. + +Opcode word layout:: + + bits [7:0] = primary opcode + bits [15:8] = sub-opcode / condition code + bits [23:16] = flags + +Compose via bitwise OR: ``opcode | (sub << 8) | flag`` +Example: ``OP_ICMP | (ICMP_SLE << 8) | FLAG_SRC1_IMM`` +""" + +# ── Flags (pre-shifted to bit 16+) ────────────────────────────────────────── +FLAG_DST_IMM = 1 << 18 # dst field is an immediate value, not a register +FLAG_SRC0_IMM = 1 << 16 # src0 field is an immediate value, not a register +FLAG_SRC1_IMM = 1 << 17 # src1 field is an immediate value, not a register +FLAG_AUX0_IMM = 1 << 19 # aux0 field is an immediate value, not a register +FLAG_AUX1_IMM = 1 << 20 # aux1 field is an immediate value, not a register +FLAG_AUX2_IMM = 1 << 21 # aux2 field is an immediate value, not a register +FLAG_AUX3_IMM = 1 << 22 # aux3 field is an immediate value, not a register + +FLAG_FLOAT = 1 << 23 # operation uses float semantics + + +# ── Control Flow ───────────────────────────────────────────────────────────── +OP_NOP = 0x00 +OP_RET = 0x02 +OP_JUMP = 0x04 +OP_BRANCH = 0x05 +OP_SWITCH = 0x06 +OP_CALL = 0x07 +OP_CALL_RETURN = 0x08 + +# ── Quantum ────────────────────────────────────────────────────────────────── +OP_QUANTUM_GATE = 0x10 +OP_MEASURE = 0x11 +OP_RESET = 0x12 +OP_READ_RESULT = 0x13 +OP_RECORD_OUTPUT = 0x14 +OP_READ_LOSS = 0x15 + +# ── Integer Arithmetic ─────────────────────────────────────────────────────── +OP_ADD = 0x20 +OP_SUB = 0x21 +OP_MUL = 0x22 +OP_UDIV = 0x23 +OP_SDIV = 0x24 +OP_UREM = 0x25 +OP_SREM = 0x26 + +# ── Bitwise / Shift ───────────────────────────────────────────────────────── +OP_AND = 0x28 +OP_OR = 0x29 +OP_XOR = 0x2A +OP_SHL = 0x2B +OP_LSHR = 0x2C +OP_ASHR = 0x2D + +# ── Comparison ─────────────────────────────────────────────────────────────── +OP_ICMP = 0x30 +OP_FCMP = 0x31 + +# ── Float Arithmetic ───────────────────────────────────────────────────────── +OP_FADD = 0x38 +OP_FSUB = 0x39 +OP_FMUL = 0x3A +OP_FDIV = 0x3B + +# ── Type Conversion ────────────────────────────────────────────────────────── +OP_ZEXT = 0x40 +OP_SEXT = 0x41 +OP_TRUNC = 0x42 +OP_FPEXT = 0x43 +OP_FPTRUNC = 0x44 +OP_INTTOPTR = 0x45 +OP_FPTOSI = 0x46 +OP_SITOFP = 0x47 + +# ── SSA / Data Movement ───────────────────────────────────────────────────── +OP_PHI = 0x50 +OP_SELECT = 0x51 +OP_MOV = 0x52 +OP_CONST = 0x53 + +# ── ICmp condition codes (sub-opcode, placed in bits[15:8] via << 8) ───────── +# Reference: https://llvm.org/docs/LangRef.html#icmp-instruction +ICMP_EQ = 0 +ICMP_NE = 1 +ICMP_SLT = 2 +ICMP_SLE = 3 +ICMP_SGT = 4 +ICMP_SGE = 5 +ICMP_ULT = 6 +ICMP_ULE = 7 +ICMP_UGT = 8 +ICMP_UGE = 9 + +# ── FCmp condition codes ───────────────────────────────────────────────────── +# Reference: https://llvm.org/docs/LangRef.html#fcmp-instruction +FCMP_FALSE = 0 +FCMP_OEQ = 1 +FCMP_OGT = 2 +FCMP_OGE = 3 +FCMP_OLT = 4 +FCMP_OLE = 5 +FCMP_ONE = 6 +FCMP_ORD = 7 +FCMP_UNO = 8 +FCMP_UEQ = 9 +FCMP_UGT = 10 +FCMP_UGE = 11 +FCMP_ULT = 12 +FCMP_ULE = 13 +FCMP_UNE = 14 +FCMP_TRUE = 15 + +# ── Register type tags ─────────────────────────────────────────────────────── +REG_TYPE_BOOL = 0 +REG_TYPE_I32 = 1 +REG_TYPE_I64 = 2 +REG_TYPE_F32 = 3 +REG_TYPE_F64 = 4 +REG_TYPE_PTR = 5 diff --git a/source/qdk_package/qdk/_adaptive_pass.py b/source/qdk_package/qdk/_adaptive_pass.py new file mode 100644 index 0000000000..11d89fcf0e --- /dev/null +++ b/source/qdk_package/qdk/_adaptive_pass.py @@ -0,0 +1,1050 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""AdaptiveProfilePass: walks Adaptive Profile QIR and emits the intermediate +format consumed by Rust. + +Unlike ``AggregateGatesPass`` (which subclasses ``pyqir.QirModuleVisitor`` and +only dispatches CALL instructions), this pass iterates basic blocks and +instructions directly so it can handle *all* LLVM IR opcodes required by the +Adaptive Profile specification. +""" + +from __future__ import annotations +from dataclasses import dataclass, astuple +from enum import Enum +import pyqir +import struct +from typing import Any, Dict, List, Optional, Tuple, TypeAlias, cast +from ._adaptive_bytecode import * + + +class Bytecode(Enum): + Bit32 = 32 + Bit64 = 64 + + +# --------------------------------------------------------------------------- +# Gate name → OpID mapping (must match shader_types.rs OpID enum) +# --------------------------------------------------------------------------- + +GATE_MAP: Dict[str, int] = { + "reset": 1, + "x": 2, + "y": 3, + "z": 4, + "h": 5, + "s": 6, + "s__adj": 7, + "t": 8, + "t__adj": 9, + "sx": 10, + "sx__adj": 11, + "rx": 12, + "ry": 13, + "rz": 14, + "cnot": 15, + "cx": 15, + "cz": 16, + "cy": 29, + "rxx": 17, + "ryy": 18, + "rzz": 19, + "ccx": 20, + "m": 21, + "mz": 21, + "mresetz": 22, + "swap": 24, + "move": 28, +} + +# Gates that take a result ID as a second argument +MEASURE_GATES = {"m", "mz", "mresetz"} + +# Gates that reset a qubit (single qubit argument, no result) +RESET_GATES = {"reset"} + +# Rotation gates that take an angle parameter as first argument +ROTATION_GATES = {"rx", "ry", "rz", "rxx", "ryy", "rzz"} + +# Single-qubit gates whose QIR signature carries device-specific extra +# arguments after the qubit pointer (e.g. ``move(qubit, i64, i64)``). The +# extra args are scheduling metadata for hardware backends and are not +# qubit IDs, so we resolve only ``args[0]`` and ignore the rest. +MOVE_GATES = {"move"} + +# --------------------------------------------------------------------------- +# ICmp / FCmp predicate mappings +# --------------------------------------------------------------------------- + +ICMP_MAP = { + pyqir.IntPredicate.EQ: ICMP_EQ, + pyqir.IntPredicate.NE: ICMP_NE, + pyqir.IntPredicate.SLT: ICMP_SLT, + pyqir.IntPredicate.SLE: ICMP_SLE, + pyqir.IntPredicate.SGT: ICMP_SGT, + pyqir.IntPredicate.SGE: ICMP_SGE, + pyqir.IntPredicate.ULT: ICMP_ULT, + pyqir.IntPredicate.ULE: ICMP_ULE, + pyqir.IntPredicate.UGT: ICMP_UGT, + pyqir.IntPredicate.UGE: ICMP_UGE, +} + +FCMP_MAP = { + pyqir.FloatPredicate.FALSE: FCMP_FALSE, + pyqir.FloatPredicate.OEQ: FCMP_OEQ, + pyqir.FloatPredicate.OGT: FCMP_OGT, + pyqir.FloatPredicate.OGE: FCMP_OGE, + pyqir.FloatPredicate.OLT: FCMP_OLT, + pyqir.FloatPredicate.OLE: FCMP_OLE, + pyqir.FloatPredicate.ONE: FCMP_ONE, + pyqir.FloatPredicate.ORD: FCMP_ORD, + pyqir.FloatPredicate.UNO: FCMP_UNO, + pyqir.FloatPredicate.UEQ: FCMP_UEQ, + pyqir.FloatPredicate.UGT: FCMP_UGT, + pyqir.FloatPredicate.UGE: FCMP_UGE, + pyqir.FloatPredicate.ULT: FCMP_ULT, + pyqir.FloatPredicate.ULE: FCMP_ULE, + pyqir.FloatPredicate.UNE: FCMP_UNE, + pyqir.FloatPredicate.TRUE: FCMP_TRUE, +} + + +@dataclass +class AdaptiveProgram: + num_qubits: int + num_results: int + num_registers: int + entry_block: int + blocks: List[Block] + instructions: List[Instruction] + quantum_ops: List[QuantumOp] + functions: List[Function] + phi_entries: List[PhiNodeEntry] + switch_cases: List[SwitchCase] + call_args: List[CallArg] + labels: List[Label] + register_types: List[RegisterType] + + def as_dict(self): + """ + Transforms the program to a dictionary, and each of + the helper dataclasses to a tuple. This format is intended + to be used in the FFI between Python and Rust. + """ + return { + "num_qubits": self.num_qubits, + "num_results": self.num_results, + "num_registers": self.num_registers, + "entry_block": self.entry_block, + "blocks": [astuple(x) for x in self.blocks], + "instructions": [astuple(x) for x in self.instructions], + "quantum_ops": [astuple(x) for x in self.quantum_ops], + "functions": [astuple(x) for x in self.functions], + "phi_entries": [astuple(x) for x in self.phi_entries], + "switch_cases": [astuple(x) for x in self.switch_cases], + "call_args": self.call_args, + "labels": self.labels, + "register_types": self.register_types, + } + + +@dataclass +class Block: + block_id: int + instr_offset: int + instr_count: int + + +@dataclass +class Instruction: + opcode: int + dst: int + src0: int + src1: int + aux0: int + aux1: int + aux2: int + aux3: int + + +@dataclass +class QuantumOp: + op_id: int + q1: int + q2: int + q3: int + # ``angle`` is stored as the raw bit pattern of an IEEE-754 float + # (encoded via ``encode_float_as_bits``) so it can be packed into the + # same integer-typed FFI table as the qubit indices. The Rust side + # reinterprets these bits as f32/f64 depending on the bytecode width. + # + # This also follows the same pattern in which floats are encoded as ints + # in the ``Instruction`` class. + angle: int + + +@dataclass +class Function: + func_entry_block: int + num_params: int + param_base: int + + +@dataclass +class PhiNodeEntry: + block_id: int + val_reg: int + + +@dataclass +class SwitchCase: + case_val: int + target_block: int + + +# OpID for correlated noise (must match shader_types.rs OpID::CorrelatedNoise) +CORRELATED_NOISE_OP_ID = 131 + +CallArg: TypeAlias = int +Label: TypeAlias = str +RegisterType: TypeAlias = int + + +@dataclass +class IntOperand: + val: int + bits: int + + def __post_init__(self): + # Mask to the appropriate word-width so negative Python ints and + # wider-than-target constants become their two's-complement + # representation at the target bit width + # (e.g. -7 → 0xFFFFFFF9 for 32-bit, 0xFFFFFFFFFFFFFFF9 for 64-bit). + # + # Note: we have no way to tell if a negative number, represented by + # pyqir as an u64 is an overflow or just a negative number. + # therefore we don't perform overflow checks here, and instead + # default to a wrapping behavior. + mask = (1 << self.bits) - 1 + self.val = self.val & mask + + +class FloatOperand: + def __init__(self, val: float, bytecode_kind: Bytecode) -> None: + self.val: int = encode_float_as_bits(val, bytecode_kind) + + +@dataclass +class Reg: + val: int # index in the registers table + + +def is_immediate(arg) -> bool: + return isinstance(arg, (IntOperand, FloatOperand)) + + +def prepare_immediate_flags( + *, dst=None, src0=None, src1=None, aux0=None, aux1=None, aux2=None, aux3=None +): + flags = 0 + if is_immediate(dst): + flags |= FLAG_DST_IMM + if is_immediate(src0): + flags |= FLAG_SRC0_IMM + if is_immediate(src1): + flags |= FLAG_SRC1_IMM + if is_immediate(aux0): + flags |= FLAG_AUX0_IMM + if is_immediate(aux1): + flags |= FLAG_AUX1_IMM + if is_immediate(aux2): + flags |= FLAG_AUX2_IMM + if is_immediate(aux3): + flags |= FLAG_AUX3_IMM + return flags + + +def unwrap_operands( + dst, src0, src1, aux0, aux1, aux2, aux3 +) -> Tuple[int, int, int, int, int, int, int]: + if not isinstance(dst, int): + dst = dst.val + if not isinstance(src0, int): + src0 = src0.val + if not isinstance(src1, int): + src1 = src1.val + if not isinstance(aux0, int): + aux0 = aux0.val + if not isinstance(aux1, int): + aux1 = aux1.val + if not isinstance(aux2, int): + aux2 = aux2.val + if not isinstance(aux3, int): + aux3 = aux3.val + return (dst, src0, src1, aux0, aux1, aux2, aux3) + + +def encode_float_as_bits(val: float, bytecode_kind: Bytecode) -> int: + if bytecode_kind == Bytecode.Bit32: + return struct.unpack(" AdaptiveProgram: + """Process module and return the AdaptiveProgram. + + :param mod: The QIR module to process. + :param noise: Optional NoiseConfig. When provided, noise intrinsic calls + are resolved to correlated noise ops using the intrinsics table. + :param noise_intrinsics: Optional dict mapping noise intrinsic callee names + to noise table IDs. Takes precedence over ``noise`` if both are given. + :return: The processed adaptive program. + :rtype: AdaptiveProgram + """ + if mod.get_flag("arrays"): + raise ValueError("QIR arrays are not currently supported.") + + if noise_intrinsics is not None: + self._noise_intrinsics = noise_intrinsics + elif noise is not None: + # Build {name: table_id} mapping from the NoiseConfig intrinsics + intrinsics = noise.intrinsics + self._noise_intrinsics = {} + for callee_name in mod.functions: + name = callee_name.name + if name in intrinsics: + self._noise_intrinsics[name] = intrinsics.get_intrinsic_id(name) + + errors = mod.verify() + if errors is not None: + raise ValueError(f"Module verification failed: {errors}") + + # Pass 1: Assign block IDs and function IDs for all defined functions + for func in mod.functions: + if len(func.basic_blocks) > 0: + self._assign_function(func) + + # Pass 2: Walk instructions and emit encoding + for func in mod.functions: + if len(func.basic_blocks) > 0: + self._walk_function(func) + + entry_func = next(filter(pyqir.is_entry_point, mod.functions)) + num_qubits = pyqir.required_num_qubits(entry_func) + num_results = pyqir.required_num_results(entry_func) + assert isinstance(num_qubits, int) + assert isinstance(num_results, int) + + return AdaptiveProgram( + num_qubits=num_qubits, + num_results=num_results, + num_registers=self._next_reg, + entry_block=self._block_to_id[entry_func.basic_blocks[0]], + blocks=self.blocks, + instructions=self.instructions, + quantum_ops=self.quantum_ops, + functions=self.functions, + phi_entries=self.phi_entries, + switch_cases=self.switch_cases, + call_args=self.call_args, + labels=self.labels, + register_types=self.register_types, + ) + + # ------------------------------------------------------------------ + # Register allocation + # ------------------------------------------------------------------ + + def _alloc_reg(self, value: Any, type_tag: int) -> Reg: + """Allocate a new register for `value` and record its type. + + If `value` was already pre-allocated (e.g. as a forward reference from + a phi node), return the existing register instead of allocating a new + one. + """ + if value is not None and value in self._value_to_reg: + return self._value_to_reg[value] + reg = Reg(self._next_reg) + self._next_reg += 1 + if value is not None: + self._value_to_reg[value] = reg + self.register_types.append(type_tag) + return reg + + # ------------------------------------------------------------------ + # Instruction emission + # ------------------------------------------------------------------ + + def _emit( + self, + opcode: int, + *, + dst: int | IntOperand | FloatOperand | Reg = 0, + src0: int | IntOperand | FloatOperand | Reg = 0, + src1: int | IntOperand | FloatOperand | Reg = 0, + aux0: int | IntOperand | FloatOperand | Reg = 0, + aux1: int | IntOperand | FloatOperand | Reg = 0, + aux2: int | IntOperand | FloatOperand | Reg = 0, + aux3: int | IntOperand | FloatOperand | Reg = 0, + ) -> None: + imm_flags = prepare_immediate_flags( + dst=dst, src0=src0, src1=src1, aux0=aux0, aux1=aux1, aux2=aux2, aux3=aux3 + ) + (dst, src0, src1, aux0, aux1, aux2, aux3) = unwrap_operands( + dst, src0, src1, aux0, aux1, aux2, aux3 + ) + ins = Instruction(opcode | imm_flags, dst, src0, src1, aux0, aux1, aux2, aux3) + self.instructions.append(ins) + + def _emit_quantum_op( + self, + op_id: int, + q1: int = 0, + q2: int = 0, + q3: int = 0, + angle: int = 0, + ) -> int: + idx = self._next_qop + self._next_qop += 1 + qop = QuantumOp(op_id, q1, q2, q3, angle) + self.quantum_ops.append(qop) + return idx + + # ------------------------------------------------------------------ + # Operand resolution + # ------------------------------------------------------------------ + + def _resolve_operand(self, value: pyqir.Value) -> IntOperand | FloatOperand | Reg: + """Resolve a pyqir Value to a register index. + + If `value` is an already-assigned SSA register, return its index. + If `value` is an integer constant, allocate a register and emit + ``OP_CONST`` to materialise it. + """ + if value in self._value_to_reg: + return self._value_to_reg[value] + + if isinstance(value, pyqir.IntConstant): + val = value.value + return IntOperand(val, self._int_bits) + + if isinstance(value, pyqir.FloatConstant): + val = value.value + return FloatOperand(val, self._bytecode_kind) + + # Forward reference (e.g. phi incoming from a later block). + # Pre-allocate a register; the defining instruction will reuse it + # via _alloc_reg's dedup check. + if isinstance(value, pyqir.Instruction): + return self._alloc_reg(value, self._type_tag(value.type)) + + # Constant expressions (e.g. inttoptr (i64 N to ptr)). + if isinstance(value, pyqir.Constant): + # Try extracting as a qubit/result pointer constant. + pid = pyqir.ptr_id(value) + if pid is not None: + return IntOperand(pid, self._int_bits) + # Null pointer + if value.is_null: + reg = self._alloc_reg(value, REG_TYPE_PTR) + self._emit(OP_CONST | FLAG_SRC0_IMM, dst=reg.val, src0=0) + return reg + + raise ValueError(f"Cannot resolve operand: {type(value).__name__}") + + def _type_tag(self, ty: Any) -> int: + """Map a pyqir Type to a register type tag.""" + if isinstance(ty, pyqir.IntType): + w = ty.width + if w == 1: + return REG_TYPE_BOOL + if w <= 32: + return REG_TYPE_I32 + return REG_TYPE_I64 + if isinstance(ty, pyqir.PointerType): + return REG_TYPE_PTR + if ty.is_double: + return REG_TYPE_F64 + # Remaining floating-point types (e.g. float/f32) + return REG_TYPE_F32 + + # ------------------------------------------------------------------ + # Binary / unary helpers + # ------------------------------------------------------------------ + + def _emit_binary(self, opcode: int, instr: Any) -> None: + """Emit a binary arithmetic/bitwise instruction.""" + dst = self._alloc_reg(instr, self._type_tag(instr.type)) + src0 = self._resolve_operand(instr.operands[0]) + src1 = self._resolve_operand(instr.operands[1]) + self._emit(opcode, dst=dst, src0=src0, src1=src1) + + def _emit_unary(self, opcode: int, instr: Any) -> None: + """Emit a unary conversion instruction.""" + dst = self._alloc_reg(instr, self._type_tag(instr.type)) + src0 = self._resolve_operand(instr.operands[0]) + self._emit(opcode, dst=dst, src0=src0) + + def _emit_sext(self, instr: Any) -> None: + """Emit OP_SEXT with source bit width in aux0.""" + dst = self._alloc_reg(instr, self._type_tag(instr.type)) + src0 = self._resolve_operand(instr.operands[0]) + src_type = instr.operands[0].type + src_bits = src_type.width if isinstance(src_type, pyqir.IntType) else 32 + self._emit(OP_SEXT, dst=dst, src0=src0, aux0=src_bits) + + # ------------------------------------------------------------------ + # Function assignment (Pass 1) + # ------------------------------------------------------------------ + + def _assign_function(self, func: pyqir.Function) -> None: + """Assign block IDs and function IDs for a function.""" + if not pyqir.is_entry_point(func) and func.name not in self._func_to_id: + func_id = len(self._func_to_id) + self._func_to_id[func.name] = func_id + for block in func.basic_blocks: + self._block_to_id[block] = self._next_block + self._next_block += 1 + + # ------------------------------------------------------------------ + # Function walking (Pass 2) + # ------------------------------------------------------------------ + + def _walk_function(self, func: pyqir.Function) -> None: + """Walk all blocks and instructions in a function, emitting bytecode.""" + self._current_func_is_entry = pyqir.is_entry_point(func) + + # For non-entry functions, register parameters as registers + if not self._current_func_is_entry: + param_base = self._next_reg + for param in func.params: + self._alloc_reg( + param, REG_TYPE_PTR + ) # params are pointers (%Qubit*, %Result*) + # Record function entry in the function table + if func.name in self._func_to_id: + func_entry_block = self._block_to_id[func.basic_blocks[0]] + f = Function(func_entry_block, len(func.params), param_base) + self.functions.append(f) + + for block in func.basic_blocks: + block_id = self._block_to_id[block] + instr_offset = len(self.instructions) + for instr in block.instructions: + self._on_instruction(instr) + # NOTE: block.terminator is already included in block.instructions + # in pyqir, so we do NOT separately process it. + instr_count = len(self.instructions) - instr_offset + blk = Block(block_id, instr_offset, instr_count) + self.blocks.append(blk) + + # ------------------------------------------------------------------ + # Instruction dispatch + # ------------------------------------------------------------------ + + def _on_instruction(self, instr: pyqir.Instruction) -> None: + """Dispatch a single instruction by opcode.""" + match instr.opcode: + case pyqir.Opcode.CALL: + self._emit_call(cast(pyqir.Call, instr)) + case pyqir.Opcode.PHI: + self._emit_phi(cast(pyqir.Phi, instr)) + case pyqir.Opcode.ICMP: + self._emit_icmp(cast(pyqir.ICmp, instr)) + case pyqir.Opcode.FCMP: + self._emit_fcmp(cast(pyqir.FCmp, instr)) + case pyqir.Opcode.SWITCH: + self._emit_switch(cast(pyqir.Switch, instr)) + case pyqir.Opcode.BR: + self._emit_branch(instr) + case pyqir.Opcode.RET: + self._emit_ret(instr) + case pyqir.Opcode.SELECT: + self._emit_select(instr) + case pyqir.Opcode.ADD: + self._emit_binary(OP_ADD, instr) + case pyqir.Opcode.SUB: + self._emit_binary(OP_SUB, instr) + case pyqir.Opcode.MUL: + self._emit_binary(OP_MUL, instr) + case pyqir.Opcode.UDIV: + self._emit_binary(OP_UDIV, instr) + case pyqir.Opcode.SDIV: + self._emit_binary(OP_SDIV, instr) + case pyqir.Opcode.UREM: + self._emit_binary(OP_UREM, instr) + case pyqir.Opcode.SREM: + self._emit_binary(OP_SREM, instr) + case pyqir.Opcode.AND: + self._emit_binary(OP_AND, instr) + case pyqir.Opcode.OR: + self._emit_binary(OP_OR, instr) + case pyqir.Opcode.XOR: + self._emit_binary(OP_XOR, instr) + case pyqir.Opcode.SHL: + self._emit_binary(OP_SHL, instr) + case pyqir.Opcode.LSHR: + self._emit_binary(OP_LSHR, instr) + case pyqir.Opcode.ASHR: + self._emit_binary(OP_ASHR, instr) + case pyqir.Opcode.ZEXT: + self._emit_unary(OP_ZEXT, instr) + case pyqir.Opcode.SEXT: + self._emit_sext(instr) + case pyqir.Opcode.TRUNC: + self._emit_unary(OP_TRUNC, instr) + case pyqir.Opcode.FADD: + self._emit_binary(OP_FADD | FLAG_FLOAT, instr) + case pyqir.Opcode.FSUB: + self._emit_binary(OP_FSUB | FLAG_FLOAT, instr) + case pyqir.Opcode.FMUL: + self._emit_binary(OP_FMUL | FLAG_FLOAT, instr) + case pyqir.Opcode.FDIV: + self._emit_binary(OP_FDIV | FLAG_FLOAT, instr) + case pyqir.Opcode.FP_EXT: + self._emit_unary(OP_FPEXT | FLAG_FLOAT, instr) + case pyqir.Opcode.FP_TRUNC: + self._emit_unary(OP_FPTRUNC | FLAG_FLOAT, instr) + case pyqir.Opcode.FP_TO_SI: + self._emit_unary(OP_FPTOSI, instr) + case pyqir.Opcode.SI_TO_FP: + self._emit_unary(OP_SITOFP | FLAG_FLOAT, instr) + case pyqir.Opcode.INT_TO_PTR: + self._emit_inttoptr(instr) + case _: + raise ValueError(f"Unsupported instruction: {instr.opcode}") + + # ------------------------------------------------------------------ + # Call dispatch + # ------------------------------------------------------------------ + + def _emit_call(self, call: pyqir.Call) -> None: + """Dispatch a CALL instruction based on callee name.""" + callee = call.callee.name + + match callee: + case "__quantum__qis__read_result__body" | "__quantum__rt__read_result": + dst = self._alloc_reg(call, REG_TYPE_BOOL) + result_reg = self._resolve_result_operand(call.args[0]) + self._emit(OP_READ_RESULT, dst=dst, src0=result_reg) + case "__quantum__rt__result_record_output": + result_reg = self._resolve_result_operand(call.args[0]) + label_str = self._extract_label(call.args[1]) + label_idx = len(self.labels) + self.labels.append(label_str) + self._emit(OP_RECORD_OUTPUT, src0=result_reg, aux0=label_idx) + case "__quantum__rt__array_record_output": + # Record structure output — pass through as-is for output formatting + count = ( + call.args[0].value + if isinstance(call.args[0], pyqir.IntConstant) + else 0 + ) + label_str = self._extract_label(call.args[1]) + label_idx = len(self.labels) + self.labels.append(label_str) + self._emit( + OP_RECORD_OUTPUT, src0=count, aux0=label_idx, aux1=1 + ) # aux1=1 -> array + case "__quantum__rt__tuple_record_output": + count = ( + call.args[0].value + if isinstance(call.args[0], pyqir.IntConstant) + else 0 + ) + label_str = self._extract_label(call.args[1]) + label_idx = len(self.labels) + self.labels.append(label_str) + self._emit( + OP_RECORD_OUTPUT, src0=count, aux0=label_idx, aux1=2 + ) # aux1=2 -> tuple + case "__quantum__rt__bool_record_output": + # Bool record output - pass through + src = self._resolve_operand(call.args[0]) + label_str = self._extract_label(call.args[1]) + label_idx = len(self.labels) + self.labels.append(label_str) + self._emit( + OP_RECORD_OUTPUT, src0=src, aux0=label_idx, aux1=3 + ) # aux1=3 -> bool + case "__quantum__rt__int_record_output": + src = self._resolve_operand(call.args[0]) + label_str = self._extract_label(call.args[1]) + label_idx = len(self.labels) + self.labels.append(label_str) + self._emit( + OP_RECORD_OUTPUT, src0=src, aux0=label_idx, aux1=4 + ) # aux1=4 -> int + case ( + "__quantum__rt__initialize" + | "__quantum__rt__begin_parallel" + | "__quantum__rt__end_parallel" + | "__quantum__qis__barrier__body" + ): + pass # No-op + case "__quantum__rt__read_loss": + # Allocate a bool register and emit OP_READ_LOSS so the runtime + # can ask the simulator whether the given result was produced + # by measuring a lost qubit. Programs may branch on this value. + dst = self._alloc_reg(call, REG_TYPE_BOOL) + result_reg = self._resolve_result_operand(call.args[0]) + self._emit(OP_READ_LOSS, dst=dst, src0=result_reg) + case _ if callee.startswith("__quantum__qis__"): + self._emit_quantum_call(call) + case _ if callee in self._func_to_id: + self._emit_ir_function_call(call) + case _ if "qdk_noise" in call.callee.attributes.func: + # Check if this is a noise intrinsic (custom gate with qdk_noise attribute) + self._emit_noise_intrinsic_call(call) + case _: + raise ValueError(f"Unsupported call: {callee}") + + # ------------------------------------------------------------------ + # Quantum call dispatch + # ------------------------------------------------------------------ + + def _resolve_qubit_operands( + self, args: List[pyqir.Value] + ) -> Tuple[IntOperand | Reg, IntOperand | Reg, IntOperand | Reg]: + qs: List[IntOperand | Reg] = [ + IntOperand(0, self._int_bits), + IntOperand(0, self._int_bits), + IntOperand(0, self._int_bits), + ] + for i, arg in enumerate(args): + qs[i] = self._resolve_qubit_operand(arg) + return (qs[0], qs[1], qs[2]) + + def _resolve_qubit_operand(self, arg: pyqir.Value) -> IntOperand | Reg: + a = self._resolve_operand(arg) + assert isinstance(a, (IntOperand, Reg)) + return a + + def _resolve_result_operand(self, arg: pyqir.Value) -> IntOperand | Reg: + a = self._resolve_operand(arg) + assert isinstance(a, (IntOperand, Reg)) + return a + + def _resolve_angle_operand(self, arg: pyqir.Value) -> FloatOperand | Reg: + a = self._resolve_operand(arg) + assert isinstance(a, (FloatOperand, Reg)) + return a + + def _emit_quantum_call(self, call: pyqir.Call) -> None: + """Emit a quantum gate, measure, or reset from a ``__quantum__qis__*`` call.""" + callee_name = call.callee.name + gate_name = callee_name.replace("__quantum__qis__", "").replace("__body", "") + op_id = GATE_MAP[gate_name] + if gate_name in MEASURE_GATES: + q = self._resolve_qubit_operand(call.args[0]) + r = self._resolve_result_operand(call.args[1]) + qop_idx = self._emit_quantum_op(op_id, q.val, r.val) + self._emit( + OP_MEASURE, + aux0=qop_idx, + aux1=q, + aux2=r, + ) + return + if gate_name in RESET_GATES: + q = self._resolve_qubit_operand(call.args[0]) + qop_idx = self._emit_quantum_op(op_id, q.val) + self._emit( + OP_RESET, + aux0=qop_idx, + aux1=q, + ) + return + if gate_name in MOVE_GATES: + # ``move(qubit, i64, i64)``: only the first arg is a qubit; the + # remaining args are device-specific scheduling metadata that + # the simulator ignores. Emit a single-qubit OP_QUANTUM_GATE so + # the runtime invokes ``Simulator::mov`` (which applies the + # configured ``noise.mov`` faults to that qubit). + q1, q2, q3 = self._resolve_qubit_operands([call.args[0]]) + angle = FloatOperand(0.0, self._bytecode_kind) + qop_idx = self._emit_quantum_op(op_id, q1.val, q2.val, q3.val, angle.val) + self._emit( + OP_QUANTUM_GATE, + src0=angle, + aux0=qop_idx, + aux1=q1, + aux2=q2, + aux3=q3, + ) + return + if gate_name in ROTATION_GATES: + qubit_arg_offset = 1 + angle = self._resolve_angle_operand(call.args[0]) + else: + qubit_arg_offset = 0 + angle = FloatOperand(0.0, self._bytecode_kind) + qubit_arg_offset = 1 if gate_name in ROTATION_GATES else 0 + q1, q2, q3 = self._resolve_qubit_operands(call.args[qubit_arg_offset:]) + qop_idx = self._emit_quantum_op(op_id, q1.val, q2.val, q3.val, angle.val) + self._emit( + OP_QUANTUM_GATE, + src0=angle, + aux0=qop_idx, + aux1=q1, + aux2=q2, + aux3=q3, + ) + + def _emit_noise_intrinsic_call(self, call: pyqir.Call) -> None: + """Emit a noise intrinsic call. + + When a noise config is provided and the callee is a known intrinsic, + store qubit register indices in ``call_args`` (following the same + pattern as ``_emit_ir_function_call``), then emit a single + ``OP_QUANTUM_GATE`` whose ``aux1`` = qubit count and ``aux2`` = + offset into ``call_args``. The shader reads qubit IDs from + ``call_arg_table`` at runtime, supporting arbitrarily many qubits. + + When no noise config is provided, emit an identity gate (no-op). + """ + callee_name = call.callee.name + if self._noise_intrinsics is not None and callee_name in self._noise_intrinsics: + table_id = self._noise_intrinsics[callee_name] + qubit_count = len(call.args) + # Store qubit register indices in call_args, materializing + # immediates into registers (same pattern as _emit_ir_function_call). + arg_offset = len(self.call_args) + for arg in call.args: + operand = self._resolve_qubit_operand(arg) + if isinstance(operand, Reg): + self.call_args.append(operand.val) + else: + reg = self._alloc_reg(None, REG_TYPE_PTR) + self._emit(OP_MOV | FLAG_SRC0_IMM, dst=reg, src0=operand.val) + self.call_args.append(reg.val) + # QuantumOp stores table_id in q1 and qubit_count in q2. + qop_idx = self._emit_quantum_op( + CORRELATED_NOISE_OP_ID, table_id, qubit_count + ) + self._emit( + OP_QUANTUM_GATE, + aux0=qop_idx, + aux1=IntOperand(qubit_count, self._int_bits), + aux2=IntOperand(arg_offset, self._int_bits), + ) + elif self._noise_intrinsics is not None: + raise ValueError(f"Missing noise intrinsic: {callee_name}") + else: + # No noise config — no-op + pass + + # ------------------------------------------------------------------ + # Control flow emitters + # ------------------------------------------------------------------ + + def _emit_branch(self, instr: pyqir.Instruction) -> None: + """Emit jump or conditional branch.""" + operands = instr.operands + if len(operands) == 1: + # Unconditional: br label %target + target = self._block_to_id[operands[0]] + self._emit(OP_JUMP, dst=target) + else: + # Conditional: br i1 %cond, label %true, label %false + # pyqir operands: [condition, FALSE_block, TRUE_block] + cond_reg = self._resolve_operand(operands[0]) + false_block = self._block_to_id[operands[1]] + true_block = self._block_to_id[operands[2]] + self._emit(OP_BRANCH, src0=cond_reg, aux0=true_block, aux1=false_block) + + def _emit_phi(self, phi_instr: pyqir.Phi) -> None: + """Emit a PHI node with side table entries.""" + dst_reg = self._alloc_reg(phi_instr, self._type_tag(phi_instr.type)) + phi_offset = len(self.phi_entries) + for value, block in phi_instr.incoming: + operand = self._resolve_operand(value) + if isinstance(operand, Reg): + val_reg = operand.val + else: + # Immediate values must be materialized into a register + # because the GPU phi_table stores register indices. + reg = self._alloc_reg(None, self._type_tag(phi_instr.type)) + self._emit(OP_MOV | FLAG_SRC0_IMM, dst=reg, src0=operand.val) + val_reg = reg.val + block_id = self._block_to_id[block] + phi_entry = PhiNodeEntry(block_id, val_reg) + self.phi_entries.append(phi_entry) + count = len(phi_instr.incoming) + self._emit(OP_PHI, dst=dst_reg, aux0=phi_offset, aux1=count) + + def _emit_select(self, instr: pyqir.Instruction) -> None: + """Emit a SELECT instruction.""" + dst = self._alloc_reg(instr, self._type_tag(instr.type)) + cond = self._resolve_operand(instr.operands[0]) + true_val = self._resolve_operand(instr.operands[1]) + false_val = self._resolve_operand(instr.operands[2]) + self._emit(OP_SELECT, dst=dst, src0=cond, aux0=true_val, aux1=false_val) + + def _emit_switch(self, switch_instr: pyqir.Switch) -> None: + """Emit a SWITCH instruction with case table entries. + + NOTE: We use ``operands`` instead of the ``.cond`` / ``.cases`` + helpers because pyqir's ``Switch.cond`` returns a stale ``Function`` + reference when ``mod.functions`` has already been iterated (two-pass + compilation). ``operands`` is not affected by this behavior. + """ + # operands layout: [cond, default_block, case_val0, case_block0, ...] + cond_reg = self._resolve_operand(switch_instr.operands[0]) + default_block = self._block_to_id[switch_instr.default] + case_offset = len(self.switch_cases) + for case_val, block in switch_instr.cases: + target_block = self._block_to_id[block] + switch_case = SwitchCase(case_val.value, target_block) + self.switch_cases.append(switch_case) + case_count = len(switch_instr.cases) + self._emit( + OP_SWITCH, + src0=cond_reg, + aux0=default_block, + aux1=case_offset, + aux2=case_count, + ) + + def _emit_ret(self, instr: Any) -> None: + """Emit RET or CALL_RETURN.""" + if not self._current_func_is_entry: + # Return from IR-defined function + if len(instr.operands) > 0: + ret_reg = self._resolve_operand(instr.operands[0]) + self._emit(OP_CALL_RETURN, src0=ret_reg) + else: + self._emit(OP_CALL_RETURN) + else: + # Return from entry point + if len(instr.operands) > 0: + ret_reg = self._resolve_operand(instr.operands[0]) + self._emit(OP_RET, dst=ret_reg) + else: + # Void return — use immediate 0 as exit code. + self._emit(OP_RET, dst=IntOperand(0, self._int_bits)) + + # ------------------------------------------------------------------ + # Comparison emitters + # ------------------------------------------------------------------ + + def _emit_icmp(self, instr: Any) -> None: + """Emit an integer comparison.""" + cond_code = ICMP_MAP.get(instr.predicate, 0) + dst = self._alloc_reg(instr, REG_TYPE_BOOL) + src0 = self._resolve_operand(instr.operands[0]) + src1 = self._resolve_operand(instr.operands[1]) + self._emit(OP_ICMP | (cond_code << 8), dst=dst, src0=src0, src1=src1) + + def _emit_fcmp(self, instr: Any) -> None: + """Emit a float comparison.""" + cond_code = FCMP_MAP.get(instr.predicate, 0) + dst = self._alloc_reg(instr, REG_TYPE_BOOL) + src0 = self._resolve_operand(instr.operands[0]) + src1 = self._resolve_operand(instr.operands[1]) + self._emit( + OP_FCMP | (cond_code << 8) | FLAG_FLOAT, + dst=dst, + src0=src0, + src1=src1, + ) + + # ------------------------------------------------------------------ + # inttoptr handling + # ------------------------------------------------------------------ + + def _emit_inttoptr(self, instr: Any) -> None: + """Handle ``inttoptr`` — just propagate the source register. + + ``inttoptr i64 %v to %Qubit*`` is a no-op cast; the integer value + is the qubit/result ID. We use OP_MOV to alias the value. + """ + src_operand = instr.operands[0] + src_reg = self._resolve_operand(src_operand) + # Register the inttoptr result as pointing to the same register + dst = self._alloc_reg(instr, REG_TYPE_PTR) + self._emit(OP_MOV, dst=dst, src0=src_reg) + + # ------------------------------------------------------------------ + # IR-defined function call/return + # ------------------------------------------------------------------ + + def _emit_ir_function_call(self, call: Any) -> None: + """Emit OP_CALL for an IR-defined function.""" + func_name = call.callee.name + func_id = self._func_to_id[func_name] + arg_offset = len(self.call_args) + for arg in call.args: + operand = self._resolve_operand(arg) + if isinstance(operand, Reg): + self.call_args.append(operand.val) + else: + # Immediate values must be materialized into a register + # because the GPU call_arg_table stores register indices. + reg = self._alloc_reg(None, REG_TYPE_PTR) + self._emit(OP_MOV | FLAG_SRC0_IMM, dst=reg, src0=operand.val) + self.call_args.append(reg.val) + # Allocate return register if function has non-void return type + if call.type.is_void: + return_reg = void_return(self._bytecode_kind) # no return + else: + return_reg = self._alloc_reg(call, REG_TYPE_I32) + self._emit( + OP_CALL, + dst=return_reg, + aux0=func_id, + aux1=len(call.args), + aux2=arg_offset, + ) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _extract_label(self, value: Any) -> str: + """Extract a label string from a call argument.""" + bs = pyqir.extract_byte_string(value) + if bs is not None: + return bs.decode("utf-8") + return "" diff --git a/source/qdk_package/qdk/_device/__init__.py b/source/qdk_package/qdk/_device/__init__.py new file mode 100644 index 0000000000..59041732f4 --- /dev/null +++ b/source/qdk_package/qdk/_device/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from ._device import Device + +__all__ = [ + "Device", +] diff --git a/source/qdk_package/qdk/_device/_atom/__init__.py b/source/qdk_package/qdk/_device/_atom/__init__.py new file mode 100644 index 0000000000..1df3925360 --- /dev/null +++ b/source/qdk_package/qdk/_device/_atom/__init__.py @@ -0,0 +1,337 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from __future__ import annotations + +from .._device import Device, Zone, ZoneType +from ..._native import try_create_gpu_adapter +from ..._types import QirInputData +from ... import telemetry_events + +from typing import List, Literal, Optional, TYPE_CHECKING +import time + +if TYPE_CHECKING: + from ...simulation._simulation import NoiseConfig + + +class NeutralAtomDevice(Device): + """ + Representation of a neutral atom device quantum computer. + """ + + def __init__( + self, + column_count: int = 40, + register_zone_row_count: int = 25, + interaction_zone_row_count: int = 2, + measurement_zone_row_count: int = 2, + ): + default_layout = ( + column_count == 40 + and register_zone_row_count == 25 + and interaction_zone_row_count == 2 + and measurement_zone_row_count == 2 + ) + telemetry_events.on_neutral_atom_init(default_layout) + + super().__init__( + column_count, + [ + Zone("Register 1", register_zone_row_count, ZoneType.REG), + Zone("Interaction Zone", interaction_zone_row_count, ZoneType.INTER), + Zone("Measurement Zone", measurement_zone_row_count, ZoneType.MEAS), + ], + ) + + def _init_home_locs(self): + # Set up the home locations for qubits in the NeutralAtomDevice layout. + assert len(self.zones) == 3 + assert ( + self.zones[0].type == ZoneType.REG + and self.zones[1].type == ZoneType.INTER + and self.zones[2].type == ZoneType.MEAS + ) + rz1_rows = range(self.zones[0].row_count - 1, -1, -1) + self.home_locs = [] + for row in range(self.zones[0].row_count): + for col in range(self.column_count): + self.home_locs.append((rz1_rows[row], col)) + + def compile( + self, + program: str | QirInputData, + verbose: bool = False, + ) -> QirInputData: + """ + Compile a QIR program for the NeutralAtomDevice device. This includes decomposing gates to the native gate set, + optimizing sequences of single qubit gates, pruning unused functions, and reordering instructions to + enable better scheduling during execution. + + :param program: The QIR program to compile, either as a string or as QirInputData. + :param verbose: If true, print detailed information about each compilation step. + :returns QirInputData: The compiled QIR program. + """ + + from ._optimize import ( + OptimizeSingleQubitGates, + PruneUnusedFunctions, + ) + from ._decomp import ( + DecomposeMultiQubitToCZ, + DecomposeSingleRotationToRz, + DecomposeSingleQubitToRzSX, + ReplaceResetWithMResetZ, + ) + from ._reorder import Reorder + from pyqir import Module, Context + + start_time = time.monotonic() + all_start_time = start_time + telemetry_events.on_neutral_atom_compile() + + name = "" + if isinstance(program, QirInputData): + name = program._name + + if verbose: + print(f"Compiling program {name} for NeutralAtomDevice device...") + + module = Module.from_ir(Context(), str(program)) + if verbose: + end_time = time.monotonic() + print(f" Loaded module in {end_time - start_time:.2f} seconds") + start_time = end_time + + OptimizeSingleQubitGates().run(module) + if verbose: + end_time = time.monotonic() + print( + f" Optimized single qubit gates in {end_time - start_time:.2f} seconds" + ) + start_time = end_time + + DecomposeMultiQubitToCZ().run(module) + if verbose: + end_time = time.monotonic() + print( + f" Decomposed multi-qubit gates to CZ in {end_time - start_time:.2f} seconds" + ) + start_time = end_time + + OptimizeSingleQubitGates().run(module) + if verbose: + end_time = time.monotonic() + print( + f" Optimized single qubit gates in {end_time - start_time:.2f} seconds" + ) + start_time = end_time + + DecomposeSingleRotationToRz().run(module) + if verbose: + end_time = time.monotonic() + print( + f" Decomposed single rotations to Rz in {end_time - start_time:.2f} seconds" + ) + start_time = end_time + + OptimizeSingleQubitGates().run(module) + if verbose: + end_time = time.monotonic() + print( + f" Optimized single qubit gates in {end_time - start_time:.2f} seconds" + ) + start_time = end_time + + DecomposeSingleQubitToRzSX().run(module) + if verbose: + end_time = time.monotonic() + print( + f" Decomposed single qubit gates to Rz and SX in {end_time - start_time:.2f} seconds" + ) + start_time = end_time + + OptimizeSingleQubitGates().run(module) + if verbose: + end_time = time.monotonic() + print( + f" Optimized single qubit gates in {end_time - start_time:.2f} seconds" + ) + start_time = end_time + + ReplaceResetWithMResetZ().run(module) + if verbose: + end_time = time.monotonic() + print( + f" Replaced resets with mresetz in {end_time - start_time:.2f} seconds" + ) + start_time = end_time + + PruneUnusedFunctions().run(module) + if verbose: + end_time = time.monotonic() + print(f" Pruned unused functions in {end_time - start_time:.2f} seconds") + start_time = end_time + + Reorder(self).run(module) + if verbose: + end_time = time.monotonic() + print(f" Reordered instructions in {end_time - start_time:.2f} seconds") + start_time = end_time + + end_time = time.monotonic() + telemetry_events.on_neutral_atom_compile_end((end_time - all_start_time) * 1000) + if verbose: + print( + f"Finished compiling program {name} in {end_time - all_start_time:.2f} seconds" + ) + + return QirInputData(name, str(module)) + + def show_trace(self, qir: str | QirInputData): + """ + Visualize the execution trace of a QIR program on the NeutralAtomDevice device using the Atoms widget. + This includes approximate layout and scheduling of the program to show the parallelism of gates and + movement of qubits during execution. + + :param qir: The QIR program to visualize, either as a string or as QirInputData. + """ + + try: + from qsharp_widgets import Atoms + except ImportError: + raise ImportError( + "The qsharp-widgets package is required for showing atom trace visualization. " + "Please install it via 'pip install \"qdk[jupyter]\"' or 'pip install qsharp-widgets'." + ) + from ._trace import Trace + from ._validate import ValidateNoConditionalBranches + from ._scheduler import Schedule + from pyqir import Module, Context + from IPython.display import display + + start_time = time.monotonic() + telemetry_events.on_neutral_atom_trace() + + # Compile and visualize the trace in one step. + compiled = self.compile(qir) + module = Module.from_ir(Context(), str(compiled)) + ValidateNoConditionalBranches().run(module) + Schedule(self).run(module) + tracer = Trace(self) + tracer.run(module) + display(Atoms(machine_layout=self.get_layout(), trace_data=tracer.trace)) + + end_time = time.monotonic() + telemetry_events.on_neutral_atom_trace_end((end_time - start_time) * 1000) + + def simulate( + self, + qir: str | QirInputData, + shots=1, + noise: NoiseConfig | None = None, + type: Optional[Literal["clifford", "cpu", "gpu"]] = None, + seed: Optional[int] = None, + ) -> List: + """ + Simulate a QIR program on the NeutralAtomDevice device. This includes approximate layout and scheduling of the program + to model the parallelism of gates and movement of qubits during execution. The simulation can optionally + include noise based on a provided noise configuration. + + :param qir: The QIR program to simulate, either as a string or as QirInputData. + :param shots: The number of shots to simulate. Defaults to 1. + :param noise: An optional NoiseConfig to include noise in the simulation. + :param type: The type of simulator to use: + Use `"clifford"` if your QIR only contains Clifford gates and measurements. + Use `"gpu"` if you have a GPU available in your system. + Use `"cpu"` as a fallback option if you don't have a GPU in your system. + If `None` (default), the GPU simulator will be tried first, falling back to + CPU if a suitable GPU device could not be located. + :param seed: An optional random seed for reproducibility. + :return: The results of each shot of the simulation as a list. + """ + + from ...simulation._simulation import ( + NoiseConfig, + run_qir_clifford, + run_qir_cpu, + run_qir_gpu, + ) + from ._validate import ValidateNoConditionalBranches + from ._scheduler import Schedule + from ._decomp import DecomposeRzAnglesToCliffordGates + from pyqir import Module, Context + + start_time = time.monotonic() + + using_noise = noise is not None + if noise is None: + noise = NoiseConfig() + + # Override t, t_adj, s, s_adj, and z noise if they are unset and rz noise is set. + if noise and not noise.rz.is_noiseless(): + if noise.t.is_noiseless(): + noise.t.x = noise.rz.x + noise.t.y = noise.rz.y + noise.t.z = noise.rz.z + noise.t.loss = noise.rz.loss + if noise.t_adj.is_noiseless(): + noise.t_adj.x = noise.rz.x + noise.t_adj.y = noise.rz.y + noise.t_adj.z = noise.rz.z + noise.t_adj.loss = noise.rz.loss + if noise.s.is_noiseless(): + noise.s.x = noise.rz.x + noise.s.y = noise.rz.y + noise.s.z = noise.rz.z + noise.s.loss = noise.rz.loss + if noise.s_adj.is_noiseless(): + noise.s_adj.x = noise.rz.x + noise.s_adj.y = noise.rz.y + noise.s_adj.z = noise.rz.z + noise.s_adj.loss = noise.rz.loss + if noise.z.is_noiseless(): + noise.z.x = noise.rz.x + noise.z.y = noise.rz.y + noise.z.z = noise.rz.z + noise.z.loss = noise.rz.loss + + compiled = self.compile(qir) + module = Module.from_ir(Context(), str(compiled)) + ValidateNoConditionalBranches().run(module) + Schedule(self).run(module) + + if type is None: + try: + try_create_gpu_adapter() + type = "gpu" + except OSError: + telemetry_events.on_neutral_atom_cpu_fallback() + type = "cpu" + + telemetry_events.on_neutral_atom_simulate(shots, using_noise, type) + + match type: + case "clifford": + DecomposeRzAnglesToCliffordGates().run(module) + result = run_qir_clifford( + str(module), + shots, + noise, + seed, + ) + case "cpu": + result = run_qir_cpu(str(module), shots, noise, seed) + case "gpu": + result = run_qir_gpu(str(module), shots, noise, seed) + case _: + raise ValueError(f"Simulation type {type} is not supported") + + end_time = time.monotonic() + telemetry_events.on_neutral_atom_simulate_end( + (end_time - start_time) * 1000, shots, using_noise, type + ) + return result + + +__all__ = ["NeutralAtomDevice"] diff --git a/source/qdk_package/qdk/_device/_atom/_decomp.py b/source/qdk_package/qdk/_device/_atom/_decomp.py new file mode 100644 index 0000000000..f51d878119 --- /dev/null +++ b/source/qdk_package/qdk/_device/_atom/_decomp.py @@ -0,0 +1,510 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from pyqir import ( + FloatConstant, + const, + Function, + FunctionType, + PointerType, + Type, + result, + Context, + Linkage, + QirModuleVisitor, + required_num_results, +) +from math import pi +from ._utils import TOLERANCE + + +class DecomposeMultiQubitToCZ(QirModuleVisitor): + """ + Decomposes all multi-qubit gates to CZ gates and single qubit gates. + """ + + h_func: Function + s_func: Function + sadj_func: Function + t_func: Function + tadj_func: Function + rz_func: Function + cz_func: Function + + def _on_module(self, module): + void = Type.void(module.context) + qubit_ty = PointerType(Type.void(module.context)) + self.double_ty = Type.double(module.context) + # Find or create all the needed functions. + for func in module.functions: + match func.name: + case "__quantum__qis__h__body": + self.h_func = func + case "__quantum__qis__s__body": + self.s_func = func + case "__quantum__qis__s__adj": + self.sadj_func = func + case "__quantum__qis__t__body": + self.t_func = func + case "__quantum__qis__t__adj": + self.tadj_func = func + case "__quantum__qis__rz__body": + self.rz_func = func + case "__quantum__qis__cz__body": + self.cz_func = func + if not hasattr(self, "h_func"): + self.h_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__h__body", + module, + ) + if not hasattr(self, "s_func"): + self.s_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__s__body", + module, + ) + if not hasattr(self, "sadj_func"): + self.sadj_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__s__adj", + module, + ) + if not hasattr(self, "t_func"): + self.t_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__t__body", + module, + ) + if not hasattr(self, "tadj_func"): + self.tadj_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__t__adj", + module, + ) + if not hasattr(self, "rz_func"): + self.rz_func = Function( + FunctionType(void, [self.double_ty, qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__rz__body", + module, + ) + if not hasattr(self, "cz_func"): + self.cz_func = Function( + FunctionType(void, [qubit_ty, qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__cz__body", + module, + ) + super()._on_module(module) + + def _on_qis_ccx(self, call, ctrl1, ctrl2, target): + self.builder.insert_before(call) + self.builder.call(self.h_func, [target]) + self.builder.call(self.tadj_func, [ctrl1]) + self.builder.call(self.tadj_func, [ctrl2]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.cz_func, [target, ctrl1]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.t_func, [ctrl1]) + self.builder.call(self.h_func, [target]) + self.builder.call(self.cz_func, [ctrl2, target]) + self.builder.call(self.h_func, [target]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.cz_func, [ctrl2, ctrl1]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.t_func, [target]) + self.builder.call(self.tadj_func, [ctrl1]) + self.builder.call(self.h_func, [target]) + self.builder.call(self.cz_func, [ctrl2, target]) + self.builder.call(self.h_func, [target]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.cz_func, [target, ctrl1]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.tadj_func, [target]) + self.builder.call(self.t_func, [ctrl1]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.cz_func, [ctrl2, ctrl1]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.h_func, [target]) + call.erase() + + def _on_qis_cx(self, call, ctrl, target): + self.builder.insert_before(call) + self.builder.call(self.h_func, [target]) + self.builder.call(self.cz_func, [ctrl, target]) + self.builder.call(self.h_func, [target]) + call.erase() + + def _on_qis_cy(self, call, ctrl, target): + self.builder.insert_before(call) + self.builder.call(self.sadj_func, [target]) + self.builder.call(self.h_func, [target]) + self.builder.call(self.cz_func, [ctrl, target]) + self.builder.call(self.h_func, [target]) + self.builder.call(self.s_func, [target]) + call.erase() + + def _on_qis_rxx(self, call, angle, target1, target2): + self.builder.insert_before(call) + self.builder.call(self.h_func, [target2]) + self.builder.call(self.cz_func, [target2, target1]) + self.builder.call(self.h_func, [target1]) + self.builder.call(self.rz_func, [angle, target1]) + self.builder.call(self.h_func, [target1]) + self.builder.call(self.cz_func, [target2, target1]) + self.builder.call(self.h_func, [target2]) + call.erase() + + def _on_qis_ryy(self, call, angle, target1, target2): + self.builder.insert_before(call) + self.builder.call(self.sadj_func, [target1]) + self.builder.call(self.sadj_func, [target2]) + self.builder.call(self.h_func, [target2]) + self.builder.call(self.cz_func, [target2, target1]) + self.builder.call(self.h_func, [target1]) + self.builder.call(self.rz_func, [angle, target1]) + self.builder.call(self.h_func, [target1]) + self.builder.call(self.cz_func, [target2, target1]) + self.builder.call(self.h_func, [target2]) + self.builder.call(self.s_func, [target2]) + self.builder.call(self.s_func, [target1]) + call.erase() + + def _on_qis_rzz(self, call, angle, target1, target2): + self.builder.insert_before(call) + self.builder.call(self.h_func, [target1]) + self.builder.call(self.cz_func, [target2, target1]) + self.builder.call(self.h_func, [target1]) + self.builder.call(self.rz_func, [angle, target1]) + self.builder.call(self.h_func, [target1]) + self.builder.call(self.cz_func, [target2, target1]) + self.builder.call(self.h_func, [target1]) + call.erase() + + def _on_qis_swap(self, call, target1, target2): + self.builder.insert_before(call) + self.builder.call(self.h_func, [target2]) + self.builder.call(self.cz_func, [target1, target2]) + self.builder.call(self.h_func, [target2]) + self.builder.call(self.h_func, [target1]) + self.builder.call(self.cz_func, [target2, target1]) + self.builder.call(self.h_func, [target1]) + self.builder.call(self.h_func, [target2]) + self.builder.call(self.cz_func, [target1, target2]) + self.builder.call(self.h_func, [target2]) + call.erase() + + +class DecomposeSingleRotationToRz(QirModuleVisitor): + """ + Decomposes all single qubit rotations to Rz gates. + """ + + h_func: Function + s_func: Function + sadj_func: Function + rz_func: Function + + def _on_module(self, module): + void = Type.void(module.context) + qubit_ty = PointerType(Type.void(module.context)) + self.double_ty = Type.double(module.context) + # Find or create all the needed functions. + for func in module.functions: + match func.name: + case "__quantum__qis__h__body": + self.h_func = func + case "__quantum__qis__s__body": + self.s_func = func + case "__quantum__qis__s__adj": + self.sadj_func = func + case "__quantum__qis__rz__body": + self.rz_func = func + if not hasattr(self, "h_func"): + self.h_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__h__body", + module, + ) + if not hasattr(self, "s_func"): + self.s_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__s__body", + module, + ) + if not hasattr(self, "sadj_func"): + self.sadj_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__s__adj", + module, + ) + if not hasattr(self, "rz_func"): + self.rz_func = Function( + FunctionType(void, [self.double_ty, qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__rz__body", + module, + ) + super()._on_module(module) + + def _on_qis_rx(self, call, angle, target): + self.builder.insert_before(call) + self.builder.call(self.h_func, [target]) + self.builder.call( + self.rz_func, + [angle, target], + ) + self.builder.call(self.h_func, [target]) + call.erase() + + def _on_qis_ry(self, call, angle, target): + self.builder.insert_before(call) + self.builder.call(self.sadj_func, [target]) + self.builder.call(self.h_func, [target]) + self.builder.call( + self.rz_func, + [angle, target], + ) + self.builder.call(self.h_func, [target]) + self.builder.call(self.s_func, [target]) + call.erase() + + +class DecomposeSingleQubitToRzSX(QirModuleVisitor): + """ + Decomposes all single qubit gates to Rz and Sx gates. + """ + + sx_func: Function + rz_func: Function + + def _on_module(self, module): + void = Type.void(module.context) + qubit_ty = PointerType(Type.void(module.context)) + self.double_ty = Type.double(module.context) + # Find or create all the needed functions. + for func in module.functions: + match func.name: + case "__quantum__qis__sx__body": + self.sx_func = func + case "__quantum__qis__rz__body": + self.rz_func = func + if not hasattr(self, "sx_func"): + self.sx_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__sx__body", + module, + ) + if not hasattr(self, "rz_func"): + self.rz_func = Function( + FunctionType(void, [self.double_ty, qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__rz__body", + module, + ) + super()._on_module(module) + + def _on_qis_h(self, call, target): + self.builder.insert_before(call) + self.builder.call( + self.rz_func, + [const(self.double_ty, pi / 2), target], + ) + self.builder.call(self.sx_func, [target]) + self.builder.call( + self.rz_func, + [const(self.double_ty, pi / 2), target], + ) + call.erase() + + def _on_qis_s(self, call, target): + self.builder.insert_before(call) + self.builder.call( + self.rz_func, + [const(self.double_ty, pi / 2), target], + ) + call.erase() + + def _on_qis_s_adj(self, call, target): + self.builder.insert_before(call) + self.builder.call( + self.rz_func, + [const(self.double_ty, -pi / 2), target], + ) + call.erase() + + def _on_qis_t(self, call, target): + self.builder.insert_before(call) + self.builder.call( + self.rz_func, + [const(self.double_ty, pi / 4), target], + ) + call.erase() + + def _on_qis_t_adj(self, call, target): + self.builder.insert_before(call) + self.builder.call( + self.rz_func, + [const(self.double_ty, -pi / 4), target], + ) + call.erase() + + def _on_qis_x(self, call, target): + self.builder.insert_before(call) + self.builder.call(self.sx_func, [target]) + self.builder.call(self.sx_func, [target]) + call.erase() + + def _on_qis_y(self, call, target): + self.builder.insert_before(call) + self.builder.call(self.sx_func, [target]) + self.builder.call(self.sx_func, [target]) + self.builder.call( + self.rz_func, + [const(self.double_ty, pi), target], + ) + call.erase() + + def _on_qis_z(self, call, target): + self.builder.insert_before(call) + self.builder.call( + self.rz_func, + [const(self.double_ty, pi), target], + ) + call.erase() + + +class DecomposeRzAnglesToCliffordGates(QirModuleVisitor): + """ + Ensure that the module only contains Clifford gates instead of rotation angles. + """ + + THREE_PI_OVER_2 = 3 * pi / 2 + PI_OVER_2 = pi / 2 + TWO_PI = 2 * pi + + z_func: Function + s_func: Function + sadj_func: Function + + def _on_module(self, module): + void = Type.void(module.context) + qubit_ty = PointerType(Type.void(module.context)) + self.double_ty = Type.double(module.context) + # Find or create all the needed functions. + for func in module.functions: + match func.name: + case "__quantum__qis__s__body": + self.s_func = func + case "__quantum__qis__s__adj": + self.sadj_func = func + case "__quantum__qis__z__body": + self.z_func = func + + if not hasattr(self, "s_func"): + self.s_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__s__body", + module, + ) + if not hasattr(self, "sadj_func"): + self.sadj_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__s__adj", + module, + ) + if not hasattr(self, "z_func"): + self.z_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__z__body", + module, + ) + + super()._on_module(module) + + def _on_qis_rz(self, call, angle, target): + if not isinstance(angle, FloatConstant): + raise ValueError("Angle used in RZ must be a constant") + angle = angle.value + + self.builder.insert_before(call) + + if ( + abs(angle - self.THREE_PI_OVER_2) < TOLERANCE + or abs(angle + self.PI_OVER_2) < TOLERANCE + ): + self.builder.call(self.sadj_func, [target]) + elif abs(angle - pi) < TOLERANCE or abs(angle + pi) < TOLERANCE: + self.builder.call(self.z_func, [target]) + elif ( + abs(angle - self.PI_OVER_2) < TOLERANCE + or abs(angle + self.THREE_PI_OVER_2) < TOLERANCE + ): + self.builder.call(self.s_func, [target]) + elif ( + angle < TOLERANCE + or abs(angle - self.TWO_PI) < TOLERANCE + or abs(angle + self.TWO_PI) < TOLERANCE + ): + # I, drop it + pass + else: + raise ValueError( + f"Angle {angle} used in RZ is not a Clifford compatible rotation angle" + ) + + call.erase() + + +class ReplaceResetWithMResetZ(QirModuleVisitor): + """ + Replaces all reset operations with a call to mresetz using a new, ignored result identifier. + """ + + context: Context + mresetz_func: Function + next_result_id: int + + def _on_module(self, module): + self.context = module.context + void = Type.void(self.context) + qubit_ty = PointerType(Type.void(self.context)) + result_ty = PointerType(Type.void(self.context)) + # Find or create the intrinsic mresetz function + for func in module.functions: + match func.name: + case "__quantum__qis__mresetz__body": + self.mresetz_func = func + if not hasattr(self, "mresetz_func"): + self.mresetz_func = Function( + FunctionType(void, [qubit_ty, result_ty]), + Linkage.EXTERNAL, + "__quantum__qis__mresetz__body", + module, + ) + super()._on_module(module) + + def _on_function(self, function): + self.next_result_id = required_num_results(function) or 0 + super()._on_function(function) + + def _on_qis_reset(self, call, target): + self.builder.insert_before(call) + # Create a new result identifier to ignore the measurement result + result_id = result(self.context, self.next_result_id) + self.next_result_id += 1 + self.builder.call(self.mresetz_func, [target, result_id]) + call.erase() diff --git a/source/pip/qsharp/_device/_atom/_optimize.py b/source/qdk_package/qdk/_device/_atom/_optimize.py similarity index 100% rename from source/pip/qsharp/_device/_atom/_optimize.py rename to source/qdk_package/qdk/_device/_atom/_optimize.py diff --git a/source/pip/qsharp/_device/_atom/_reorder.py b/source/qdk_package/qdk/_device/_atom/_reorder.py similarity index 100% rename from source/pip/qsharp/_device/_atom/_reorder.py rename to source/qdk_package/qdk/_device/_atom/_reorder.py diff --git a/source/pip/qsharp/_device/_atom/_scheduler.py b/source/qdk_package/qdk/_device/_atom/_scheduler.py similarity index 100% rename from source/pip/qsharp/_device/_atom/_scheduler.py rename to source/qdk_package/qdk/_device/_atom/_scheduler.py diff --git a/source/pip/qsharp/_device/_atom/_trace.py b/source/qdk_package/qdk/_device/_atom/_trace.py similarity index 100% rename from source/pip/qsharp/_device/_atom/_trace.py rename to source/qdk_package/qdk/_device/_atom/_trace.py diff --git a/source/pip/qsharp/_device/_atom/_utils.py b/source/qdk_package/qdk/_device/_atom/_utils.py similarity index 100% rename from source/pip/qsharp/_device/_atom/_utils.py rename to source/qdk_package/qdk/_device/_atom/_utils.py diff --git a/source/qdk_package/qdk/_device/_atom/_validate.py b/source/qdk_package/qdk/_device/_atom/_validate.py new file mode 100644 index 0000000000..0ebab719f8 --- /dev/null +++ b/source/qdk_package/qdk/_device/_atom/_validate.py @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from pyqir import QirModuleVisitor, is_entry_point, Opcode + + +class ValidateAllowedIntrinsics(QirModuleVisitor): + """ + Ensure that the module only contains allowed intrinsics. + """ + + def _on_function(self, function): + name = function.name + if ( + not is_entry_point(function) + and not name.endswith("_record_output") + and name + not in [ + "__quantum__rt__begin_parallel", + "__quantum__rt__end_parallel", + "__quantum__qis__read_result__body", + "__quantum__rt__read_result", + "__quantum__qis__move__body", + "__quantum__qis__cz__body", + "__quantum__qis__sx__body", + "__quantum__qis__rz__body", + "__quantum__qis__mresetz__body", + ] + ): + raise ValueError(f"{name} is not a supported intrinsic") + + +class ValidateNoConditionalBranches(QirModuleVisitor): + """ + Ensure that the function(s) only use unconditional branches. + """ + + def _on_block(self, block): + if ( + block.terminator + and block.terminator.opcode == Opcode.BR + and len(block.terminator.operands) > 1 + ): + raise ValueError("programs with branching control flow are not supported") + super()._on_block(block) diff --git a/source/pip/qsharp/_device/_device.py b/source/qdk_package/qdk/_device/_device.py similarity index 99% rename from source/pip/qsharp/_device/_device.py rename to source/qdk_package/qdk/_device/_device.py index 991dc46b24..e0030fa18a 100644 --- a/source/pip/qsharp/_device/_device.py +++ b/source/qdk_package/qdk/_device/_device.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. from enum import Enum -from .._qsharp import QirInputData +from .._types import QirInputData class ZoneType(Enum): diff --git a/source/qdk_package/qdk/_fs.py b/source/qdk_package/qdk/_fs.py new file mode 100644 index 0000000000..c317007fd1 --- /dev/null +++ b/source/qdk_package/qdk/_fs.py @@ -0,0 +1,90 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +_fs.py + +This module provides file system utility functions for working with the file +system as Python sees it. These are used as callbacks passed into native code +to allow the native code to interact with the file system in an +environment-specific way. +""" + +import os +from typing import Dict, List, Tuple + + +def read_file(path: str) -> Tuple[str, str]: + """ + Read the contents of a file. + + :param path: The path to the file. + :return: A tuple containing the path and the file contents. + :rtype: Tuple[str, str] + """ + with open(path, mode="r", encoding="utf-8-sig") as f: + return (path, f.read()) + + +def list_directory(dir_path: str) -> List[Dict[str, str]]: + """ + Lists the contents of a directory and returns a list of dictionaries, + where each dictionary represents an entry in the directory. + + :param dir_path: The path of the directory to list. + :return: A list of dictionaries representing the entries in the directory. + Each dictionary contains the following keys: + - ``"path"``: The full path of the entry. + - ``"entry_name"``: The name of the entry. + - ``"type"``: The type of the entry: ``"file"``, ``"folder"``, or ``"unknown"``. + :rtype: List[Dict[str, str]] + """ + + def map_dir(e: str) -> Dict[str, str]: + path = os.path.join(dir_path, e) + return { + "path": path, + "entry_name": e, + "type": ( + "file" + if os.path.isfile(path) + else "folder" if os.path.isdir(path) else "unknown" + ), + } + + return list(map(map_dir, os.listdir(dir_path))) + + +def resolve(base: str, path: str) -> str: + """ + Resolves a relative path with respect to a base path. + + :param base: The base path. + :param path: The relative path. + :return: The resolved path. + :rtype: str + """ + return os.path.normpath(join(base, path)) + + +def exists(path) -> bool: + """ + Check if a file or directory exists at the given path. + + :param path: The path to the file or directory. + :return: ``True`` if the file or directory exists, ``False`` otherwise. + :rtype: bool + """ + return os.path.exists(path) + + +def join(path: str, *paths) -> str: + """ + Joins one or more path components intelligently. + + :param path: The base path. + :param *paths: Additional path components to be joined. + :return: The concatenated path. + :rtype: str + """ + return os.path.join(path, *paths) diff --git a/source/qdk_package/qdk/_http.py b/source/qdk_package/qdk/_http.py new file mode 100644 index 0000000000..240ddcc67f --- /dev/null +++ b/source/qdk_package/qdk/_http.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +_http.py + +This module provides HTTP utility functions for interacting with +GitHub repositories. +""" + + +def fetch_github(owner: str, repo: str, ref: str, path: str) -> str: + """ + Fetches the content of a file from a GitHub repository. + + :param owner: The owner of the GitHub repository. + :param repo: The name of the GitHub repository. + :param ref: The reference (branch, tag, or commit) of the repository. + :param path: The path to the file within the repository. + :return: The content of the file as a string. + :rtype: str + :raises urllib.error.HTTPError: If there is an error fetching the file from GitHub. + :raises urllib.error.URLError: If there is an error with the URL. + """ + + import urllib.request + + path_no_leading_slash = path[1:] if path.startswith("/") else path + url = f"https://raw.githubusercontent.com/{owner}/{repo}/{ref}/{path_no_leading_slash}" + return urllib.request.urlopen(url).read().decode("utf-8-sig") diff --git a/source/qdk_package/qdk/_interpreter.py b/source/qdk_package/qdk/_interpreter.py new file mode 100644 index 0000000000..4f3f19ecca --- /dev/null +++ b/source/qdk_package/qdk/_interpreter.py @@ -0,0 +1,1060 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Q# interpreter lifecycle and core operations. + +This module manages the singleton Q# interpreter instance and exposes the +public functions that drive it: :func:`init`, :func:`eval`, :func:`run`, +:func:`compile`, :func:`circuit`, :func:`estimate`, :func:`logical_counts`, +:func:`set_quantum_seed`, :func:`set_classical_seed`, :func:`dump_machine`, +and :func:`dump_circuit`. + +Internal helpers such as :func:`get_interpreter`, :func:`ipython_helper`, +:func:`python_args_to_interpreter_args`, and +:func:`qsharp_value_to_python_value` are also defined here for use by other +submodules. +""" + +import warnings +from . import telemetry_events, code +from ._native import ( # type: ignore + Interpreter, + TargetProfile, + StateDumpData, + QSharpError, + Output, + Circuit, + GlobalCallable, + Closure, + Pauli, + Result, + UdtValue, + TypeIR, + TypeKind, + PrimitiveKind, + CircuitConfig, + CircuitGenerationMethod, + NoiseConfig, +) +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + Set, + Tuple, + Union, + Iterable, + cast, +) +from .estimator._estimator import ( + EstimatorResult, + EstimatorParams, + LogicalCounts, +) +from ._types import ( + PauliNoise, + DepolarizingNoise, + BitFlipNoise, + PhaseFlipNoise, + StateDump, + ShotResult, + Config, + QirInputData, +) +import json +import sys +import types +from time import monotonic +from dataclasses import make_dataclass + +# --------------------------------------------------------------------------- +# Value conversion helpers +# --------------------------------------------------------------------------- + + +def lower_python_obj(obj: object, visited: Optional[Set[object]] = None) -> Any: + if visited is None: + visited = set() + + if id(obj) in visited: + raise QSharpError("Cannot send circular objects from Python to Q#.") + + visited = visited.copy().add(id(obj)) + + # Base case: Primitive types + if isinstance(obj, (bool, int, float, complex, str, Pauli, Result)): + return obj + + # Recursive case: Tuple + if isinstance(obj, tuple): + return tuple(lower_python_obj(elt, visited) for elt in obj) + + # Recursive case: Dict + if isinstance(obj, dict): + return {name: lower_python_obj(val, visited) for name, val in obj.items()} + + # Base case: Callable or Closure + if hasattr(obj, "__global_callable"): + return obj.__getattribute__("__global_callable") + if isinstance(obj, (GlobalCallable, Closure)): + return obj + + # Recursive case: Class with slots + if hasattr(obj, "__slots__"): + fields = {} + for name in getattr(obj, "__slots__"): + if name == "__dict__": + for name, val in obj.__dict__.items(): + fields[name] = lower_python_obj(val, visited) + else: + val = getattr(obj, name) + fields[name] = lower_python_obj(val, visited) + return fields + + # Recursive case: Class + if hasattr(obj, "__dict__"): + fields = { + name: lower_python_obj(val, visited) for name, val in obj.__dict__.items() + } + return fields + + # Recursive case: Array + # By using `Iterable` instead of `list`, we can handle other kind of iterables + # like numpy arrays and generators. + if isinstance(obj, Iterable): + return [lower_python_obj(elt, visited) for elt in obj] + + raise TypeError(f"unsupported type: {type(obj)}") + + +def python_args_to_interpreter_args(args): + """ + Helper function to turn the `*args` argument of this module + to the format expected by the Q# interpreter. + """ + if len(args) == 0: + return None + elif len(args) == 1: + return lower_python_obj(args[0]) + else: + return lower_python_obj(args) + + +def qsharp_value_to_python_value(obj): + # Base case: Primitive types + if isinstance(obj, (bool, int, float, complex, str, Pauli, Result)): + return obj + + # Recursive case: Tuple + if isinstance(obj, tuple): + # Special case Value::UNIT maps to None. + if not obj: + return None + return tuple(qsharp_value_to_python_value(elt) for elt in obj) + + # Recursive case: Array + if isinstance(obj, list): + return [qsharp_value_to_python_value(elt) for elt in obj] + + # Recursive case: Callable or Closure + if isinstance(obj, (GlobalCallable, Closure)): + return obj + + # Recursive case: Udt + if isinstance(obj, UdtValue): + class_name = obj.name + fields = [] + args = [] + for name, value_ir in obj.fields: + val = qsharp_value_to_python_value(value_ir) + ty = type(val) + args.append(val) + fields.append((name, ty)) + return make_dataclass(class_name, fields)(*args) + + +def make_class_rec(qsharp_type: TypeIR) -> type: + class_name = qsharp_type.unwrap_udt().name + fields = {} + for field in qsharp_type.unwrap_udt().fields: + ty = None + kind = field[1].kind() + + if kind == TypeKind.Primitive: + prim_kind = field[1].unwrap_primitive() + if prim_kind == PrimitiveKind.Bool: + ty = bool + elif prim_kind == PrimitiveKind.Int: + ty = int + elif prim_kind == PrimitiveKind.Double: + ty = float + elif prim_kind == PrimitiveKind.Complex: + ty = complex + elif prim_kind == PrimitiveKind.String: + ty = str + elif prim_kind == PrimitiveKind.Pauli: + ty = Pauli + elif prim_kind == PrimitiveKind.Result: + ty = Result + else: + raise QSharpError(f"unknown primitive {prim_kind}") + elif kind == TypeKind.Tuple: + # Special case Value::UNIT maps to None. + if not field[1].unwrap_tuple(): + ty = type(None) + else: + ty = tuple + elif kind == TypeKind.Array: + ty = list + elif kind == TypeKind.Udt: + ty = make_class_rec(field[1]) + else: + raise QSharpError(f"unknown type {kind}") + fields[field[0]] = ty + + return make_dataclass( + class_name, + fields, + ) + + +# --------------------------------------------------------------------------- +# Interpreter singleton +# --------------------------------------------------------------------------- + + +_interpreter: Union["Interpreter", None] = None +_config: Union["Config", None] = None + +# Check if we are running in a Jupyter notebook to use the IPython display function +_in_jupyter = False +try: + from IPython.display import display + + if get_ipython().__class__.__name__ == "ZMQInteractiveShell": # type: ignore + _in_jupyter = True # Jupyter notebook or qtconsole +except: + pass + + +# Reporting execution time during IPython cells requires that IPython +# gets pinged to ensure it understands the cell is active. This is done by +# simply importing the display function, which it turns out is enough to begin timing +# while avoiding any UI changes that would be visible to the user. +def ipython_helper(): + try: + if __IPYTHON__: # type: ignore + from IPython.display import display + except NameError: + pass + + +def init( + *, + target_profile: TargetProfile = TargetProfile.Unrestricted, + target_name: Optional[str] = None, + project_root: Optional[str] = None, + language_features: Optional[List[str]] = None, + trace_circuit: Optional[bool] = None, +) -> Config: + """ + Initializes the Q# interpreter. + + :keyword target_profile: Setting the target profile allows the Q# + interpreter to generate programs that are compatible + with a specific target. See :class:`TargetProfile`. + + :keyword target_name: An optional name of the target machine to use for inferring the compatible + target_profile setting. + + :keyword project_root: An optional path to a root directory with a Q# project to include. + It must contain a qsharp.json project manifest. + + :keyword language_features: An optional list of language feature flags to enable. + These correspond to experimental or preview Q# language features. + Valid values are: + + - ``"v2-preview-syntax"``: Enables Q# v2 preview syntax. This removes support for + the scoped qubit allocation block form (``use q = Qubit() { ... }``), requiring + the statement form instead (``use q = Qubit();``). It also removes the requirement + to use the ``set`` keyword for mutable variable assignments. + + :keyword trace_circuit: Enables tracing of circuit during execution. + Passing `True` is required for the `dump_circuit` function to return a circuit. + The `circuit` function is *NOT* affected by this parameter will always generate a circuit. + :return: The Q# interpreter configuration. + :rtype: Config + """ + from ._fs import read_file, list_directory, exists, join, resolve + from ._http import fetch_github + + global _interpreter + global _config + + if isinstance(target_name, str): + target = target_name.split(".")[0].lower() + if target == "ionq" or target == "rigetti": + target_profile = TargetProfile.Base + elif target == "quantinuum": + target_profile = TargetProfile.Adaptive_RI + else: + raise QSharpError( + f'target_name "{target_name}" not recognized. Please set target_profile directly.' + ) + + manifest_contents = None + if project_root is not None: + # Normalize the project path (i.e. fix file separators and remove unnecessary '.' and '..') + project_root = resolve(".", project_root) + qsharp_json = join(project_root, "qsharp.json") + if not exists(qsharp_json): + raise QSharpError( + f"{qsharp_json} not found. qsharp.json should exist at the project root and be a valid JSON file." + ) + + try: + _, manifest_contents = read_file(qsharp_json) + except Exception as e: + raise QSharpError( + f"Error reading {qsharp_json}. qsharp.json should exist at the project root and be a valid JSON file." + ) from e + + # Loop through the environment module and remove any dynamically added attributes that represent + # Q# callables or structs. This is necessary to avoid conflicts with the new interpreter instance. + keys_to_remove = [] + for key, val in code.__dict__.items(): + if ( + hasattr(val, "__global_callable") + or hasattr(val, "__qsharp_class") + or isinstance(val, types.ModuleType) + ): + keys_to_remove.append(key) + for key in keys_to_remove: + code.__delattr__(key) + + # Also remove any namespace modules dynamically added to the system. + keys_to_remove = [] + for key in sys.modules: + if key.startswith("qdk.code."): + keys_to_remove.append(key) + for key in keys_to_remove: + sys.modules.__delitem__(key) + + _interpreter = Interpreter( + target_profile, + language_features, + project_root, + read_file, + list_directory, + resolve, + fetch_github, + _make_callable, + _make_class, + trace_circuit, + ) + + _config = Config(target_profile, language_features, manifest_contents, project_root) + # Return the configuration information to provide a hint to the + # language service through the cell output. + return _config + + +def get_interpreter() -> Interpreter: + """ + Returns the Q# interpreter. + + :return: The Q# interpreter. + :rtype: Interpreter + """ + global _interpreter + if _interpreter is None: + init() + assert _interpreter is not None, "Failed to initialize the Q# interpreter." + return _interpreter + + +def get_config() -> Config: + """ + Returns the Q# interpreter configuration. + + :return: The Q# interpreter configuration. + :rtype: Config + """ + global _config + if _config is None: + init() + assert _config is not None, "Failed to initialize the Q# interpreter." + return _config + + +# --------------------------------------------------------------------------- +# Callable / class factory helpers (used by native code) +# --------------------------------------------------------------------------- + + +# Helper function that knows how to create a function that invokes a callable. This will be +# used by the underlying native code to create functions for callables on the fly that know +# how to get the currently initialized global interpreter instance. +def _make_callable(callable: GlobalCallable, namespace: List[str], callable_name: str): + module = code + # Create a name that will be used to collect the hierarchy of namespace identifiers if they exist and use that + # to register created modules with the system. + accumulated_namespace = "qdk.code" + accumulated_namespace += "." + for name in namespace: + accumulated_namespace += name + # Use the existing entry, which should already be a module. + if hasattr(module, name): + module = module.__getattribute__(name) + if sys.modules.get(accumulated_namespace) is None: + # This is an existing entry that is not yet registered in sys.modules, so add it. + # This can happen if a callable with the same name as this namespace is already + # defined. + sys.modules[accumulated_namespace] = module + else: + # This namespace entry doesn't exist as a module yet, so create it, add it to the environment, and + # add it to sys.modules so it supports import properly. + new_module = types.ModuleType(accumulated_namespace) + module.__setattr__(name, new_module) + sys.modules[accumulated_namespace] = new_module + module = new_module + accumulated_namespace += "." + + def _callable(*args): + ipython_helper() + + def callback(output: Output) -> None: + if _in_jupyter: + try: + display(output) + return + except: + # If IPython is not available, fall back to printing the output + pass + print(output, flush=True) + + args = python_args_to_interpreter_args(args) + + output = get_interpreter().invoke(callable, args, callback) + return qsharp_value_to_python_value(output) + + # Each callable is annotated so that we know it is auto-generated and can be removed on a re-init of the interpreter. + _callable.__global_callable = callable + + # Add the callable to the module. + if module.__dict__.get(callable_name) is None: + module.__setattr__(callable_name, _callable) + else: + # Preserve any existing attributes on the attribute with the matching name, + # since this could be a collision with an existing namespace/module. + for key, val in module.__dict__.get(callable_name).__dict__.items(): + if key != "__global_callable": + _callable.__dict__[key] = val + module.__setattr__(callable_name, _callable) + + +def _make_class(qsharp_type: TypeIR, namespace: List[str], class_name: str): + """ + Helper function to create a python class given a description of it. This will be + used by the underlying native code to create classes on the fly corresponding to + the currently initialized interpreter instance. + """ + + module = code + # Create a name that will be used to collect the hierarchy of namespace identifiers if they exist and use that + # to register created modules with the system. + accumulated_namespace = "qdk.code" + accumulated_namespace += "." + for name in namespace: + accumulated_namespace += name + # Use the existing entry, which should already be a module. + if hasattr(module, name): + module = module.__getattribute__(name) + else: + # This namespace entry doesn't exist as a module yet, so create it, add it to the environment, and + # add it to sys.modules so it supports import properly. + new_module = types.ModuleType(accumulated_namespace) + module.__setattr__(name, new_module) + sys.modules[accumulated_namespace] = new_module + module = new_module + accumulated_namespace += "." + + QSharpClass = make_class_rec(qsharp_type) + + # Each class is annotated so that we know it is auto-generated and can be removed on a re-init of the interpreter. + QSharpClass.__qsharp_class = True + + # Add the class to the module. + module.__setattr__(class_name, QSharpClass) + + +# --------------------------------------------------------------------------- +# Public API functions +# --------------------------------------------------------------------------- + + +def eval( + source: str, + *, + save_events: bool = False, +) -> Any: + """ + Evaluates Q# source code. + + Output is printed to console. + + :param source: The Q# source code to evaluate. + :keyword save_events: If true, all output will be saved and returned. If false, they will be printed. + :return: The value returned by the last statement in the source code, or the saved output if ``save_events`` is true. + :rtype: Any + :raises QSharpError: If there is an error evaluating the source code. + """ + ipython_helper() + + results: ShotResult = { + "events": [], + "result": None, + "messages": [], + "matrices": [], + "dumps": [], + } + + def on_save_events(output: Output) -> None: + # Append the output to the last shot's output list + if output.is_matrix(): + results["events"].append(output) + results["matrices"].append(output) + elif output.is_state_dump(): + dump_data = cast(StateDumpData, output.state_dump()) + state_dump = StateDump(dump_data) + results["events"].append(state_dump) + results["dumps"].append(state_dump) + elif output.is_message(): + stringified = str(output) + results["events"].append(stringified) + results["messages"].append(stringified) + + def callback(output: Output) -> None: + if _in_jupyter: + try: + display(output) + return + except: + # If IPython is not available, fall back to printing the output + pass + print(output, flush=True) + + telemetry_events.on_eval() + start_time = monotonic() + + output = get_interpreter().interpret( + source, on_save_events if save_events else callback + ) + results["result"] = qsharp_value_to_python_value(output) + + durationMs = (monotonic() - start_time) * 1000 + telemetry_events.on_eval_end(durationMs) + + if save_events: + return results + else: + return results["result"] + + +def run( + entry_expr: Union[str, Callable, GlobalCallable, Closure], + shots: int, + *args, + on_result: Optional[Callable[[ShotResult], None]] = None, + save_events: bool = False, + noise: Optional[ + Union[ + Tuple[float, float, float], + PauliNoise, + BitFlipNoise, + PhaseFlipNoise, + DepolarizingNoise, + NoiseConfig, + ] + ] = None, + qubit_loss: Optional[float] = None, + seed: Optional[int] = None, +) -> List[Any]: + """ + Runs the given Q# expression for the given number of shots. + Each shot uses an independent instance of the simulator. + + :param entry_expr: The entry expression. Alternatively, a callable can be provided, + which must be a Q# callable. + :param shots: The number of shots to run. + :param *args: The arguments to pass to the callable, if one is provided. + :param on_result: A callback function that will be called with each result. + :param save_events: If true, the output of each shot will be saved. If false, they will be printed. + :param noise: The noise to use in simulation. + :param qubit_loss: The probability of qubit loss in simulation. + :param seed: The seed to use for the random number generator in simulation, if any. + + :return: A list of results or runtime errors. If ``save_events`` is true, a list of ``ShotResult`` is returned. + :rtype: List[Any] + :raises QSharpError: If there is an error interpreting the input. + :raises ValueError: If the number of shots is less than 1. + """ + ipython_helper() + + if shots < 1: + raise ValueError("The number of shots must be greater than 0.") + + telemetry_events.on_run( + shots, + noise=(noise is not None and noise != (0.0, 0.0, 0.0)), + qubit_loss=(qubit_loss is not None and qubit_loss > 0.0), + ) + start_time = monotonic() + + results: List[ShotResult] = [] + + def print_output(output: Output) -> None: + if _in_jupyter: + try: + display(output) + return + except: + # If IPython is not available, fall back to printing the output + pass + print(output, flush=True) + + def on_save_events(output: Output) -> None: + # Append the output to the last shot's output list + results[-1]["events"].append(output) + if output.is_matrix(): + results[-1]["matrices"].append(output) + elif output.is_state_dump(): + dump_data = cast(StateDumpData, output.state_dump()) + results[-1]["dumps"].append(StateDump(dump_data)) + elif output.is_message(): + results[-1]["messages"].append(str(output)) + + callable = None + run_entry_expr = None + if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): + args = python_args_to_interpreter_args(args) + callable = entry_expr.__global_callable + elif isinstance(entry_expr, (GlobalCallable, Closure)): + args = python_args_to_interpreter_args(args) + callable = entry_expr + else: + assert isinstance(entry_expr, str) + run_entry_expr = entry_expr + + noise_config = None + if isinstance(noise, NoiseConfig): + noise_config = noise + noise = None + + shot_seed = seed + for shot in range(shots): + # We also don't want every shot to return the same results, so we update the seed for + # the next shot with the shot number. This keeps the behavior deterministic if a seed + # was provided. + if seed is not None: + shot_seed = shot + seed + + results.append( + {"result": None, "events": [], "messages": [], "matrices": [], "dumps": []} + ) + run_results = get_interpreter().run( + run_entry_expr, + on_save_events if save_events else print_output, + noise_config, + noise, + qubit_loss, + callable, + args, + shot_seed, + ) + run_results = qsharp_value_to_python_value(run_results) + results[-1]["result"] = run_results + if on_result: + on_result(results[-1]) + # For every shot after the first, treat the entry expression as None to trigger + # a rerun of the last executed expression without paying the cost for any additional + # compilation. + run_entry_expr = None + + durationMs = (monotonic() - start_time) * 1000 + telemetry_events.on_run_end(durationMs, shots) + + if save_events: + return results + else: + return [shot["result"] for shot in results] + + +def compile( + entry_expr: Union[str, Callable, GlobalCallable, Closure], *args +) -> QirInputData: + """ + Compiles the Q# source code into a program that can be submitted to a target. + Either an entry expression or a callable with arguments must be provided. + + :param entry_expr: The Q# expression that will be used as the entrypoint + for the program. Alternatively, a callable can be provided, which must + be a Q# callable. + :param *args: The arguments to pass to the callable, if one is provided. + + :return: The compiled program. Use ``str()`` to get the QIR string. + :rtype: QirInputData + + Example: + + .. code-block:: python + program = qsharp.compile("...") + with open('myfile.ll', 'w') as file: + file.write(str(program)) + """ + ipython_helper() + start = monotonic() + interpreter = get_interpreter() + target_profile = get_config().get_target_profile() + telemetry_events.on_compile(target_profile) + if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): + args = python_args_to_interpreter_args(args) + ll_str = interpreter.qir(callable=entry_expr.__global_callable, args=args) + elif isinstance(entry_expr, (GlobalCallable, Closure)): + args = python_args_to_interpreter_args(args) + ll_str = interpreter.qir(callable=entry_expr, args=args) + else: + assert isinstance(entry_expr, str) + ll_str = interpreter.qir(entry_expr=entry_expr) + res = QirInputData("main", ll_str) + durationMs = (monotonic() - start) * 1000 + telemetry_events.on_compile_end(durationMs, target_profile) + return res + + +def circuit( + entry_expr: Optional[Union[str, Callable, GlobalCallable, Closure]] = None, + *args, + operation: Optional[str] = None, + generation_method: Optional[CircuitGenerationMethod] = None, + max_operations: Optional[int] = None, + source_locations: bool = False, + group_by_scope: bool = True, + prune_classical_qubits: bool = False, +) -> Circuit: + """ + Synthesizes a circuit for a Q# program. Either an entry + expression or an operation must be provided. + + :param entry_expr: An entry expression. Alternatively, a callable can be provided, + which must be a Q# callable. + :type entry_expr: str or Callable + + :param *args: The arguments to pass to the callable, if one is provided. + + :keyword operation: The operation to synthesize. This can be a name of + an operation or a lambda expression. The operation must take only + qubits or arrays of qubits as parameters. + :kwtype operation: str + + :keyword generation_method: The method to use for circuit generation. + :attr:`~qsharp.CircuitGenerationMethod.ClassicalEval` evaluates classical + control flow at circuit generation time. + :attr:`~qsharp.CircuitGenerationMethod.Simulate` runs a full simulation to + trace the circuit. + :attr:`~qsharp.CircuitGenerationMethod.Static` uses partial evaluation and + requires a non-``Unrestricted`` target profile. Defaults to ``None`` which + auto-selects the generation method. + :kwtype generation_method: :class:`~qsharp.CircuitGenerationMethod` + + :keyword max_operations: The maximum number of operations to include in the circuit. + Defaults to ``None`` which means no limit. + :kwtype max_operations: int + + :keyword source_locations: If ``True``, annotates each gate with its source location. + :kwtype source_locations: bool + + :keyword group_by_scope: If ``True``, groups operations by their containing scope, such as function declarations or loop blocks. + :kwtype group_by_scope: bool + + :keyword prune_classical_qubits: If ``True``, removes qubits that are never used in a quantum + gate (e.g. qubits only used as classical controls). + :kwtype prune_classical_qubits: bool + + :return: The synthesized circuit. + :rtype: :class:`~qsharp._native.Circuit` + :raises QSharpError: If there is an error synthesizing the circuit. + """ + ipython_helper() + start = monotonic() + telemetry_events.on_circuit() + config = CircuitConfig( + max_operations=max_operations, + generation_method=generation_method, + source_locations=source_locations, + group_by_scope=group_by_scope, + prune_classical_qubits=prune_classical_qubits, + ) + + if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): + args = python_args_to_interpreter_args(args) + res = get_interpreter().circuit( + config=config, callable=entry_expr.__global_callable, args=args + ) + elif isinstance(entry_expr, (GlobalCallable, Closure)): + args = python_args_to_interpreter_args(args) + res = get_interpreter().circuit(config=config, callable=entry_expr, args=args) + else: + assert entry_expr is None or isinstance(entry_expr, str) + res = get_interpreter().circuit(config, entry_expr, operation=operation) + + durationMs = (monotonic() - start) * 1000 + telemetry_events.on_circuit_end(durationMs) + + return res + + +def estimate( + entry_expr: Union[str, Callable, GlobalCallable, Closure], + params: Optional[Union[Dict[str, Any], List, EstimatorParams]] = None, + *args, +) -> EstimatorResult: + """ + Estimates resources for Q# source code. + Either an entry expression or a callable with arguments must be provided. + + :param entry_expr: The entry expression. Alternatively, a callable can be provided, + which must be a Q# callable. + :param params: The parameters to configure physical estimation. + + :return: The estimated resources. + :rtype: EstimatorResult + """ + + ipython_helper() + + warnings.warn( + "This version of QRE is deprecated and will be removed in a future release. " + "Please use the new version of QRE in qdk.qre. Refer to aka.ms/qdk.QREv3 for more information.", + DeprecationWarning, + stacklevel=2, + ) + + def _coerce_estimator_params( + params: Optional[ + Union[Dict[str, Any], List[Dict[str, Any]], EstimatorParams] + ] = None, + ) -> List[Dict[str, Any]]: + if params is None: + return [{}] + elif isinstance(params, EstimatorParams): + if params.has_items: + return cast(List[Dict[str, Any]], params.as_dict()["items"]) + else: + return [params.as_dict()] + elif isinstance(params, dict): + return [params] + return params + + params = _coerce_estimator_params(params) + param_str = json.dumps(params) + telemetry_events.on_estimate() + start = monotonic() + if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): + args = python_args_to_interpreter_args(args) + res_str = get_interpreter().estimate( + param_str, callable=entry_expr.__global_callable, args=args + ) + elif isinstance(entry_expr, (GlobalCallable, Closure)): + args = python_args_to_interpreter_args(args) + res_str = get_interpreter().estimate(param_str, callable=entry_expr, args=args) + else: + assert isinstance(entry_expr, str) + res_str = get_interpreter().estimate(param_str, entry_expr=entry_expr) + res = json.loads(res_str) + + try: + qubits = res[0]["logicalCounts"]["numQubits"] + except (KeyError, IndexError): + qubits = "unknown" + + durationMs = (monotonic() - start) * 1000 + telemetry_events.on_estimate_end(durationMs, qubits) + return EstimatorResult(res) + + +def logical_counts( + entry_expr: Union[str, Callable, GlobalCallable, Closure], + *args, +) -> LogicalCounts: + """ + Extracts logical resource counts from Q# source code. + Either an entry expression or a callable with arguments must be provided. + + :param entry_expr: The entry expression. Alternatively, a callable can be provided, + which must be a Q# callable. + + :return: Program resources in terms of logical gate counts. + :rtype: LogicalCounts + """ + + ipython_helper() + + if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): + args = python_args_to_interpreter_args(args) + res_dict = get_interpreter().logical_counts( + callable=entry_expr.__global_callable, args=args + ) + elif isinstance(entry_expr, (GlobalCallable, Closure)): + args = python_args_to_interpreter_args(args) + res_dict = get_interpreter().logical_counts(callable=entry_expr, args=args) + else: + assert isinstance(entry_expr, str) + res_dict = get_interpreter().logical_counts(entry_expr=entry_expr) + return LogicalCounts(res_dict) + + +def set_quantum_seed(seed: Optional[int]) -> None: + """ + Sets the seed for the random number generator used for quantum measurements. + This applies to all Q# code executed, compiled, or estimated. + + :param seed: The seed to use for the quantum random number generator. + If None, the seed will be generated from entropy. + """ + get_interpreter().set_quantum_seed(seed) + + +def set_classical_seed(seed: Optional[int]) -> None: + """ + Sets the seed for the random number generator used for standard + library classical random number operations. + This applies to all Q# code executed, compiled, or estimated. + + :param seed: The seed to use for the classical random number generator. + If None, the seed will be generated from entropy. + """ + get_interpreter().set_classical_seed(seed) + + +def dump_machine() -> StateDump: + """ + Returns the sparse state vector of the simulator as a StateDump object. + + :return: The state of the simulator. + :rtype: StateDump + """ + ipython_helper() + return StateDump(get_interpreter().dump_machine()) + + +def dump_circuit() -> Circuit: + """ + Dumps a circuit showing the current state of the simulator. + + This circuit will contain the gates that have been applied + in the simulator up to the current point. + + Requires the interpreter to be initialized with `trace_circuit=True`. + + :return: The current circuit trace. + :rtype: Circuit + :raises QSharpError: If the interpreter was not initialized with ``trace_circuit=True``. + """ + ipython_helper() + return get_interpreter().dump_circuit() + + +def dump_operation(operation: str, num_qubits: int) -> List[List[complex]]: + """ + Returns a square matrix of complex numbers representing the operation performed. + + :param operation: The operation to be performed, which must operate on a list of qubits. + :param num_qubits: The number of qubits to be used. + + :return: The matrix representing the operation. + :rtype: List[List[complex]] + """ + import math + + code_str = f"""{{\n let op = {operation};\n use (targets, extra) = (Qubit[{num_qubits}], Qubit[{num_qubits}]);\n for i in 0..{num_qubits}-1 {{\n H(targets[i]);\n CNOT(targets[i], extra[i]);\n }}\n operation ApplyOp (op : (Qubit[] => Unit), targets : Qubit[]) : Unit {{ op(targets); }}\n ApplyOp(op, targets);\n Microsoft.Quantum.Diagnostics.DumpMachine();\n ResetAll(targets + extra);\n }}""" + result = run(code_str, shots=1, save_events=True)[0] + state = result["events"][-1].state_dump().get_dict() + num_entries = pow(2, num_qubits) + factor = math.sqrt(num_entries) + ndigits = 6 + matrix = [] + for i in range(num_entries): + matrix += [[]] + for j in range(num_entries): + entry = state.get(i * num_entries + j) + if entry is None: + matrix[i] += [complex(0, 0)] + else: + matrix[i] += [ + complex( + round(factor * entry.real, ndigits), + round(factor * entry.imag, ndigits), + ) + ] + return matrix + + +# --------------------------------------------------------------------------- +# __all__ +# --------------------------------------------------------------------------- + +__all__ = [ + # Types (re-exported from _types for convenience) + "PauliNoise", + "DepolarizingNoise", + "BitFlipNoise", + "PhaseFlipNoise", + "StateDump", + "ShotResult", + "Config", + "QirInputData", + # Native types re-exported + "Interpreter", + "TargetProfile", + "QSharpError", + "Output", + "Circuit", + "GlobalCallable", + "Closure", + "Pauli", + "Result", + "CircuitConfig", + "CircuitGenerationMethod", + "NoiseConfig", + "StateDumpData", + # Estimator types + "EstimatorResult", + "EstimatorParams", + "LogicalCounts", + # Interpreter lifecycle + "init", + "get_interpreter", + "get_config", + # Core operations + "eval", + "run", + "compile", + "circuit", + "estimate", + "logical_counts", + # Seed / state + "set_quantum_seed", + "set_classical_seed", + "dump_machine", + "dump_circuit", + "dump_operation", + # Helpers (used by other submodules) + "ipython_helper", + "python_args_to_interpreter_args", + "qsharp_value_to_python_value", + "lower_python_obj", + "make_class_rec", +] diff --git a/source/qdk_package/qdk/_ipython.py b/source/qdk_package/qdk/_ipython.py new file mode 100644 index 0000000000..d2140aa2e6 --- /dev/null +++ b/source/qdk_package/qdk/_ipython.py @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +_ipython.py + +This module provides IPython magic functions for integrating Q# code +execution within Jupyter notebooks. +""" + +from time import monotonic +from IPython.display import display, clear_output +from IPython.core.magic import register_cell_magic +from ._native import QSharpError +from ._interpreter import get_interpreter, qsharp_value_to_python_value +from . import telemetry_events + + +def register_magic(): + @register_cell_magic + def qsharp(line, cell): + """Cell magic to interpret Q# code in Jupyter notebooks.""" + # This effectively pings the kernel to ensure it recognizes the cell is running and helps with + # accureate cell execution timing. + clear_output() + + def callback(output): + display(output) + # This is a workaround to ensure that the output is flushed. This avoids an issue + # where the output is not displayed until the next output is generated or the cell + # is finished executing. + display(display_id=True) + + telemetry_events.on_run_cell() + start_time = monotonic() + + try: + results = qsharp_value_to_python_value( + get_interpreter().interpret(cell, callback) + ) + + durationMs = (monotonic() - start_time) * 1000 + telemetry_events.on_run_cell_end(durationMs) + + return results + except QSharpError as e: + # pylint: disable=raise-missing-from + raise QSharpCellError(str(e)) + + +class QSharpCellError(BaseException): + """ + Error raised when a %%qsharp cell fails. + """ + + def __init__(self, traceback: str): + self.traceback = traceback.splitlines() + + def _render_traceback_(self): + # We want to specifically override the traceback so that + # the Q# error directly from the interpreter is shown + # instead of the Python error. + return self.traceback diff --git a/source/pip/qsharp/_native.pyi b/source/qdk_package/qdk/_native.pyi similarity index 100% rename from source/pip/qsharp/_native.pyi rename to source/qdk_package/qdk/_native.pyi diff --git a/source/qdk_package/qdk/_types.py b/source/qdk_package/qdk/_types.py new file mode 100644 index 0000000000..6ed7b01710 --- /dev/null +++ b/source/qdk_package/qdk/_types.py @@ -0,0 +1,337 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Core type definitions for the qdk package. + +This module contains the pure-Python types that are used across the qdk +package. They have no dependency on the interpreter lifecycle and can be +imported freely by any submodule. + +Types defined here: + +- :class:`PauliNoise`, :class:`DepolarizingNoise`, :class:`BitFlipNoise`, + :class:`PhaseFlipNoise` — noise models for simulation. +- :class:`StateDump` — sparse state-vector snapshot. +- :class:`ShotResult` — per-shot output container. +- :class:`Config` — interpreter configuration / language-service hint. +- :class:`QirInputData` — compiled QIR wrapper for azure-quantum submission. +""" + +import os +from pathlib import Path +from typing import ( + Any, + Dict, + List, + Optional, + TypedDict, + Union, +) + +from ._native import ( # type: ignore + Output, + StateDumpData, + TargetProfile, +) + +# --------------------------------------------------------------------------- +# Noise models +# --------------------------------------------------------------------------- + + +class PauliNoise(tuple): + """ + The Pauli noise to use in simulation represented + as probabilities of Pauli-X, Pauli-Y, and Pauli-Z errors + """ + + def __new__(cls, x: float, y: float, z: float): + """ + Creates a new :class:`PauliNoise` instance with the given error probabilities. + + :param x: Probability of a Pauli-X (bit flip) error. Must be non-negative. + :type x: float + :param y: Probability of a Pauli-Y error. Must be non-negative. + :type y: float + :param z: Probability of a Pauli-Z (phase flip) error. Must be non-negative. + :type z: float + :return: A new :class:`PauliNoise` tuple ``(x, y, z)``. + :rtype: PauliNoise + :raises ValueError: If any probability is negative or if ``x + y + z > 1``. + """ + if x < 0 or y < 0 or z < 0: + raise ValueError("Pauli noise probabilities must be non-negative.") + if x + y + z > 1: + raise ValueError("The sum of Pauli noise probabilities must be at most 1.") + return super().__new__(cls, (x, y, z)) + + +class DepolarizingNoise(PauliNoise): + """ + The depolarizing noise to use in simulation. + """ + + def __new__(cls, p: float): + """ + Creates a new :class:`DepolarizingNoise` instance. + + The depolarizing channel applies Pauli-X, Pauli-Y, or Pauli-Z errors each with + probability ``p / 3``. + + :param p: Total depolarizing error probability. Must satisfy ``0 ≤ p ≤ 1``. + :type p: float + :return: A new :class:`DepolarizingNoise` with equal X, Y, and Z error probabilities. + :rtype: DepolarizingNoise + :raises ValueError: If ``p`` is negative or ``p > 1``. + """ + return super().__new__(cls, p / 3, p / 3, p / 3) + + +class BitFlipNoise(PauliNoise): + """ + The bit flip noise to use in simulation. + """ + + def __new__(cls, p: float): + """ + Creates a new :class:`BitFlipNoise` instance. + + The bit flip channel applies a Pauli-X error with probability ``p``. + + :param p: Probability of a bit flip (Pauli-X) error. Must satisfy ``0 ≤ p ≤ 1``. + :type p: float + :return: A new :class:`BitFlipNoise` with X error probability ``p``. + :rtype: BitFlipNoise + :raises ValueError: If ``p`` is negative or ``p > 1``. + """ + return super().__new__(cls, p, 0, 0) + + +class PhaseFlipNoise(PauliNoise): + """ + The phase flip noise to use in simulation. + """ + + def __new__(cls, p: float): + """ + Creates a new :class:`PhaseFlipNoise` instance. + + The phase flip channel applies a Pauli-Z error with probability ``p``. + + :param p: Probability of a phase flip (Pauli-Z) error. Must satisfy ``0 ≤ p ≤ 1``. + :type p: float + :return: A new :class:`PhaseFlipNoise` with Z error probability ``p``. + :rtype: PhaseFlipNoise + :raises ValueError: If ``p`` is negative or ``p > 1``. + """ + return super().__new__(cls, 0, 0, p) + + +# --------------------------------------------------------------------------- +# State dump +# --------------------------------------------------------------------------- + + +class StateDump: + """ + A state dump returned from the Q# interpreter. + """ + + """ + The number of allocated qubits at the time of the dump. + """ + qubit_count: int + + __inner: dict + __data: StateDumpData + + def __init__(self, data: StateDumpData): + self.__data = data + self.__inner = data.get_dict() + self.qubit_count = data.qubit_count + + def __getitem__(self, index: int) -> complex: + return self.__inner.__getitem__(index) + + def __iter__(self): + return self.__inner.__iter__() + + def __len__(self) -> int: + return len(self.__inner) + + def __repr__(self) -> str: + return self.__data.__repr__() + + def __str__(self) -> str: + return self.__data.__str__() + + def _repr_markdown_(self) -> str: + return self.__data._repr_markdown_() + + def check_eq( + self, state: Union[Dict[int, complex], List[complex]], tolerance: float = 1e-10 + ) -> bool: + """ + Checks if the state dump is equal to the given state. This is not mathematical equality, + as the check ignores global phase. + + :param state: The state to check against, provided either as a dictionary of state indices to complex amplitudes, + or as a list of real amplitudes. + :param tolerance: The tolerance for the check. Defaults to 1e-10. + :return: ``True`` if the state dump is equal to the given state within the given tolerance, ignoring global phase. + :rtype: bool + """ + phase = None + # Convert a dense list of real amplitudes to a dictionary of state indices to complex amplitudes + if isinstance(state, list): + state = {i: val for i, val in enumerate(state)} + # Filter out zero states from the state dump and the given state based on tolerance + state = {k: v for k, v in state.items() if abs(v) > tolerance} + inner_state = {k: v for k, v in self.__inner.items() if abs(v) > tolerance} + if len(state) != len(inner_state): + return False + for key in state: + if key not in inner_state: + return False + if phase is None: + # Calculate the phase based on the first state pair encountered. + # Every pair of states after this must have the same phase for the states to be equivalent. + phase = inner_state[key] / state[key] + elif abs(phase - inner_state[key] / state[key]) > tolerance: + # This pair of states does not have the same phase, + # within tolerance, so the equivalence check fails. + return False + return True + + def as_dense_state(self) -> List[complex]: + """ + Returns the state dump as a dense list of complex amplitudes. This will include zero amplitudes. + + :return: A dense list of complex amplitudes, one per computational basis state. + :rtype: List[complex] + """ + return [self.__inner.get(i, complex(0)) for i in range(2**self.qubit_count)] + + +# --------------------------------------------------------------------------- +# Shot result +# --------------------------------------------------------------------------- + + +class ShotResult(TypedDict): + """ + A single result of a shot. + """ + + events: List[Output | StateDump | str] + result: Any + messages: List[str] + matrices: List[Output] + dumps: List[StateDump] + + +# --------------------------------------------------------------------------- +# Interpreter configuration +# --------------------------------------------------------------------------- + + +class Config: + """ + Configuration hints for the language service. + """ + + _config: Dict[str, Any] + + def __init__( + self, + target_profile: TargetProfile, + language_features: Optional[List[str]], + manifest: Optional[str], + project_root: Optional[str], + ): + if target_profile == TargetProfile.Adaptive_RI: + self._config = {"targetProfile": "adaptive_ri"} + elif target_profile == TargetProfile.Adaptive_RIF: + self._config = {"targetProfile": "adaptive_rif"} + elif target_profile == TargetProfile.Adaptive_RIFLA: + self._config = {"targetProfile": "adaptive_rifla"} + elif target_profile == TargetProfile.Base: + self._config = {"targetProfile": "base"} + elif target_profile == TargetProfile.Unrestricted: + self._config = {"targetProfile": "unrestricted"} + + if language_features is not None: + self._config["languageFeatures"] = language_features + if manifest is not None: + self._config["manifest"] = manifest + if project_root: + # For now, we only support local project roots, so use a file schema in the URI. + # In the future, we may support other schemes, such as github, if/when + # we have VS Code Web + Jupyter support. + self._config["projectRoot"] = Path(os.getcwd(), project_root).as_uri() + + def __repr__(self) -> str: + return "Q# initialized with configuration: " + str(self._config) + + # See https://ipython.readthedocs.io/en/stable/config/integrating.html#rich-display + # See https://ipython.org/ipython-doc/3/notebook/nbformat.html#display-data + # This returns a custom MIME-type representation of the Q# configuration. + # This data will be available in the cell output, but will not be displayed + # to the user, as frontends would not know how to render the custom MIME type. + # Editor services that interact with the notebook frontend + # (i.e. the language service) can read and interpret the data. + def _repr_mimebundle_( + self, include: Union[Any, None] = None, exclude: Union[Any, None] = None + ) -> Dict[str, Dict[str, Any]]: + return {"application/x.qsharp-config": self._config} + + def get_target_profile(self) -> str: + """ + Returns the target profile as a string, or "unspecified" if not set. + """ + return self._config.get("targetProfile", "unspecified") + + +# --------------------------------------------------------------------------- +# QIR input data +# --------------------------------------------------------------------------- + + +# Class that wraps generated QIR, which can be used by +# azure-quantum as input data. +# +# This class must implement the QirRepresentable protocol +# that is defined by the azure-quantum package. +# See: https://github.com/microsoft/qdk-python/blob/fcd63c04aa871e49206703bbaa792329ffed13c4/azure-quantum/azure/quantum/target/target.py#L21 +class QirInputData: + # The name of this variable is defined + # by the protocol and must remain unchanged. + _name: str + + def __init__(self, name: str, ll_str: str): + self._name = name + self._ll_str = ll_str + + # The name of this method is defined + # by the protocol and must remain unchanged. + def _repr_qir_(self, **kwargs) -> bytes: + return self._ll_str.encode("utf-8") + + def __str__(self) -> str: + return self._ll_str + + +# --------------------------------------------------------------------------- +# __all__ +# --------------------------------------------------------------------------- + +__all__ = [ + "PauliNoise", + "DepolarizingNoise", + "BitFlipNoise", + "PhaseFlipNoise", + "StateDump", + "ShotResult", + "Config", + "QirInputData", +] diff --git a/source/qdk_package/qdk/applications/__init__.py b/source/qdk_package/qdk/applications/__init__.py index ef21e907f9..59e481eb93 100644 --- a/source/qdk_package/qdk/applications/__init__.py +++ b/source/qdk_package/qdk/applications/__init__.py @@ -1,25 +1,2 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - -# flake8: noqa F403 -# pyright: ignore[reportWildcardImportFromLibrary] - -"""Quantum applications for the Q# ecosystem. - -This module re-exports all public symbols from [qsharp.applications](:mod:`qsharp.applications`), -making them available under the ``qdk.applications`` namespace. - -Requires the ``applications`` extra: ``pip install "qdk[applications]"``. - -Example: - - from qdk.applications import QSharpApplication -""" - -try: - # Re-export the top-level qsharp.applications names. - from qsharp.applications import * -except Exception as ex: - raise ImportError( - "qdk.applications requires the applications extras. Install with 'pip install \"qdk[applications]\"'." - ) from ex diff --git a/source/qdk_package/qdk/applications/magnets.py b/source/qdk_package/qdk/applications/magnets.py deleted file mode 100644 index 76f7cf94fb..0000000000 --- a/source/qdk_package/qdk/applications/magnets.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -# flake8: noqa F403 -# pyright: ignore[reportWildcardImportFromLibrary] - -"""Magnetic system applications for the Q# ecosystem. - -This module re-exports all public symbols from [qsharp.applications.magnets](:mod:`qsharp.applications.magnets`), -making them available under the ``qdk.applications.magnets`` namespace. It -provides classes for modeling and simulating magnetic systems such as the Ising -model using quantum algorithms. - -Requires the ``applications`` extra: ``pip install "qdk[applications]"``. - -Example: - - from qdk.applications.magnets import IsingModel -""" - -try: - # Re-export the top-level qsharp.applications.magnets names. - from qsharp.applications.magnets import * -except Exception as ex: - raise ImportError( - "qdk.applications.magnets requires the applications extras. Install with 'pip install \"qdk[applications]\"'." - ) from ex diff --git a/source/qdk_package/qdk/applications/magnets/__init__.py b/source/qdk_package/qdk/applications/magnets/__init__.py new file mode 100644 index 0000000000..56c536659e --- /dev/null +++ b/source/qdk_package/qdk/applications/magnets/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# flake8: noqa F403 +# pyright: ignore[reportWildcardImportFromLibrary] + +"""Magnets application module. + +Re-exports from the submodules.""" + +from .geometry import * +from .models import * +from .trotter import * +from .utilities import * diff --git a/source/pip/qsharp/applications/magnets/geometry/__init__.py b/source/qdk_package/qdk/applications/magnets/geometry/__init__.py similarity index 100% rename from source/pip/qsharp/applications/magnets/geometry/__init__.py rename to source/qdk_package/qdk/applications/magnets/geometry/__init__.py diff --git a/source/pip/qsharp/applications/magnets/geometry/complete.py b/source/qdk_package/qdk/applications/magnets/geometry/complete.py similarity index 100% rename from source/pip/qsharp/applications/magnets/geometry/complete.py rename to source/qdk_package/qdk/applications/magnets/geometry/complete.py diff --git a/source/pip/qsharp/applications/magnets/geometry/lattice1d.py b/source/qdk_package/qdk/applications/magnets/geometry/lattice1d.py similarity index 100% rename from source/pip/qsharp/applications/magnets/geometry/lattice1d.py rename to source/qdk_package/qdk/applications/magnets/geometry/lattice1d.py diff --git a/source/pip/qsharp/applications/magnets/geometry/lattice2d.py b/source/qdk_package/qdk/applications/magnets/geometry/lattice2d.py similarity index 100% rename from source/pip/qsharp/applications/magnets/geometry/lattice2d.py rename to source/qdk_package/qdk/applications/magnets/geometry/lattice2d.py diff --git a/source/pip/qsharp/applications/magnets/models/__init__.py b/source/qdk_package/qdk/applications/magnets/models/__init__.py similarity index 100% rename from source/pip/qsharp/applications/magnets/models/__init__.py rename to source/qdk_package/qdk/applications/magnets/models/__init__.py diff --git a/source/pip/qsharp/applications/magnets/models/model.py b/source/qdk_package/qdk/applications/magnets/models/model.py old mode 100755 new mode 100644 similarity index 100% rename from source/pip/qsharp/applications/magnets/models/model.py rename to source/qdk_package/qdk/applications/magnets/models/model.py diff --git a/source/pip/qsharp/applications/magnets/trotter/__init__.py b/source/qdk_package/qdk/applications/magnets/trotter/__init__.py similarity index 100% rename from source/pip/qsharp/applications/magnets/trotter/__init__.py rename to source/qdk_package/qdk/applications/magnets/trotter/__init__.py diff --git a/source/pip/qsharp/applications/magnets/trotter/trotter.py b/source/qdk_package/qdk/applications/magnets/trotter/trotter.py similarity index 100% rename from source/pip/qsharp/applications/magnets/trotter/trotter.py rename to source/qdk_package/qdk/applications/magnets/trotter/trotter.py diff --git a/source/pip/qsharp/applications/magnets/utilities/__init__.py b/source/qdk_package/qdk/applications/magnets/utilities/__init__.py similarity index 100% rename from source/pip/qsharp/applications/magnets/utilities/__init__.py rename to source/qdk_package/qdk/applications/magnets/utilities/__init__.py diff --git a/source/pip/qsharp/applications/magnets/utilities/hypergraph.py b/source/qdk_package/qdk/applications/magnets/utilities/hypergraph.py similarity index 100% rename from source/pip/qsharp/applications/magnets/utilities/hypergraph.py rename to source/qdk_package/qdk/applications/magnets/utilities/hypergraph.py diff --git a/source/pip/qsharp/applications/magnets/utilities/pauli.py b/source/qdk_package/qdk/applications/magnets/utilities/pauli.py similarity index 100% rename from source/pip/qsharp/applications/magnets/utilities/pauli.py rename to source/qdk_package/qdk/applications/magnets/utilities/pauli.py diff --git a/source/qdk_package/qdk/cirq.py b/source/qdk_package/qdk/cirq.py deleted file mode 100644 index 228a2a0804..0000000000 --- a/source/qdk_package/qdk/cirq.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -"""Cirq interoperability for the Q# ecosystem. - -This module re-exports all public symbols from [qsharp.interop.cirq](:mod:`qsharp.interop.cirq`), -making them available under the ``qdk.cirq`` namespace. The primary export -is :class:`~qsharp.interop.cirq.NeutralAtomSampler` — a -standard ``cirq.Sampler`` that runs Cirq circuits on the local -NeutralAtomDevice simulator. - -For full API documentation see [qsharp.interop.cirq](:mod:`qsharp.interop.cirq`). - -Requires the ``cirq`` extra: ``pip install qdk[cirq]``. - -Usage: - - import cirq - from qdk.cirq import NeutralAtomSampler - - q0, q1 = cirq.LineQubit.range(2) - circuit = cirq.Circuit([ - cirq.H(q0), - cirq.CNOT(q0, q1), - cirq.measure(q0, q1, key="m"), - ]) - - sampler = NeutralAtomSampler(seed=42) - result = sampler.run(circuit, repetitions=1000) - print(result.histogram(key="m")) -""" - -try: - from qsharp.interop.cirq import * # pyright: ignore[reportWildcardImportFromLibrary] -except Exception as ex: - raise ImportError( - "qdk.cirq requires the cirq extra. Install with 'pip install qdk[cirq]'." - ) from ex diff --git a/source/qdk_package/qdk/cirq/__init__.py b/source/qdk_package/qdk/cirq/__init__.py new file mode 100644 index 0000000000..8a484fc8ab --- /dev/null +++ b/source/qdk_package/qdk/cirq/__init__.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Cirq interoperability for the Q# ecosystem. + +This module provides a :class:`~qsharp.interop.cirq.NeutralAtomSampler` — a standard +``cirq.Sampler`` that runs Cirq circuits on the local NeutralAtomDevice +simulator. + +Usage: + + import cirq + from qsharp.interop.cirq import NeutralAtomSampler + + q0, q1 = cirq.LineQubit.range(2) + circuit = cirq.Circuit([ + cirq.H(q0), + cirq.CNOT(q0, q1), + cirq.measure(q0, q1, key="m"), + ]) + + sampler = NeutralAtomSampler(seed=42) + result = sampler.run(circuit, repetitions=1000) + print(result.histogram(key="m")) +""" + +from ._neutral_atom import NeutralAtomSampler +from ._result import NeutralAtomCirqResult + +__all__ = [ + "NeutralAtomSampler", + "NeutralAtomCirqResult", +] diff --git a/source/pip/qsharp/interop/cirq/_neutral_atom.py b/source/qdk_package/qdk/cirq/_neutral_atom.py similarity index 89% rename from source/pip/qsharp/interop/cirq/_neutral_atom.py rename to source/qdk_package/qdk/cirq/_neutral_atom.py index 8829e44b9d..9384415689 100644 --- a/source/pip/qsharp/interop/cirq/_neutral_atom.py +++ b/source/qdk_package/qdk/cirq/_neutral_atom.py @@ -12,8 +12,8 @@ from ._result import NeutralAtomCirqResult, measurement_dict, to_cirq_result if TYPE_CHECKING: - from qsharp._simulation import NoiseConfig - from qsharp._device._atom import NeutralAtomDevice + from ..simulation import NoiseConfig + from .._device._atom import NeutralAtomDevice class NeutralAtomSampler(cirq.Sampler): @@ -32,8 +32,8 @@ class NeutralAtomSampler(cirq.Sampler): Example:: import cirq - from qsharp.interop.cirq import NeutralAtomSampler - from qsharp._simulation import NoiseConfig + from qdk.cirq import NeutralAtomSampler + from qdk.simulation import NoiseConfig q0, q1 = cirq.LineQubit.range(2) circuit = cirq.Circuit([ @@ -54,7 +54,7 @@ class NeutralAtomSampler(cirq.Sampler): result = sampler.run(circuit, repetitions=1000) print(f"Accepted: {len(result.measurements['m'])} / {len(result.raw_shots)}") - :keyword noise: Optional :class:`~qsharp._simulation.NoiseConfig` describing + :keyword noise: Optional :class:`~qdk.simulation.NoiseConfig` describing per-gate noise. The device decomposes gates to the native set ``{Rz, SX, CZ, MResetZ}``; configure noise on those native gates. For example, a Cirq ``X`` gate arriving via QASM 2.0 is decomposed @@ -69,7 +69,7 @@ class NeutralAtomSampler(cirq.Sampler): :kwtype simulator_type: str :keyword seed: Optional integer seed for reproducibility. Defaults to ``None``. :kwtype seed: int - :keyword device: An existing :class:`~qsharp._device._atom.NeutralAtomDevice` + :keyword device: An existing :class:`~qdk._device._atom.NeutralAtomDevice` instance to reuse across calls. A default-configured device is created lazily on the first call when not provided. :kwtype device: NeutralAtomDevice @@ -91,7 +91,7 @@ def __init__( def _get_device(self) -> "NeutralAtomDevice": """Return the NeutralAtomDevice, creating a default one on first access.""" if self._device is None: - from qsharp._device._atom import NeutralAtomDevice + from .._device._atom import NeutralAtomDevice self._device = NeutralAtomDevice() return self._device @@ -127,10 +127,10 @@ def _run_once( param_resolver: cirq.ParamResolver, repetitions: int, ) -> NeutralAtomCirqResult: - from qsharp._native import compile_qasm_program_to_qir - from qsharp._fs import read_file, list_directory, resolve - from qsharp._http import fetch_github - from qsharp._qsharp import TargetProfile + from .._native import compile_qasm_program_to_qir + from .._fs import read_file, list_directory, resolve + from .._http import fetch_github + from .._native import TargetProfile # Resolve parameters resolved_circuit = cirq.resolve_parameters(circuit, param_resolver) diff --git a/source/pip/qsharp/interop/cirq/_result.py b/source/qdk_package/qdk/cirq/_result.py similarity index 99% rename from source/pip/qsharp/interop/cirq/_result.py rename to source/qdk_package/qdk/cirq/_result.py index d76bf8ecc3..5e780062af 100644 --- a/source/pip/qsharp/interop/cirq/_result.py +++ b/source/qdk_package/qdk/cirq/_result.py @@ -12,7 +12,6 @@ import cirq import numpy as np - # --------------------------------------------------------------------------- # Result type # --------------------------------------------------------------------------- @@ -156,7 +155,7 @@ def _qir_display_to_bitstring(obj: Any) -> str: """ # Handle qsharp.Result enum values produced by the local simulator. try: - from qsharp import Result as _Result + from qdk._native import Result as _Result if obj == _Result.One: return "1" diff --git a/source/qdk_package/qdk/code/__init__.py b/source/qdk_package/qdk/code/__init__.py new file mode 100644 index 0000000000..695b54fb63 --- /dev/null +++ b/source/qdk_package/qdk/code/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Code module that receives any user-defined Q# callables as Python functions. +""" diff --git a/source/pip/qsharp/code/__init__.pyi b/source/qdk_package/qdk/code/__init__.pyi similarity index 100% rename from source/pip/qsharp/code/__init__.pyi rename to source/qdk_package/qdk/code/__init__.pyi diff --git a/source/qdk_package/qdk/estimator.py b/source/qdk_package/qdk/estimator.py deleted file mode 100644 index 2bec068820..0000000000 --- a/source/qdk_package/qdk/estimator.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -"""Resource estimation utilities for the Q# ecosystem. - -This module re-exports all public symbols from [qsharp.estimator](:mod:`qsharp.estimator`), -making them available under the ``qdk.estimator`` namespace. It provides -classes for configuring and interpreting Microsoft Resource Estimator jobs. - -Key exports: - -- :class:`~qsharp.estimator.EstimatorParams` — top-level input parameters for a resource estimation job. -- :class:`~qsharp.estimator.EstimatorResult` — result container with formatted tables and diagrams. -- :class:`~qsharp.estimator.LogicalCounts` — pre-calculated logical resource counts for physical estimation. -- :class:`~qsharp.estimator.QubitParams`, :class:`~qsharp.estimator.QECScheme` — predefined model name constants. -- :class:`~qsharp.estimator.EstimatorQubitParams`, :class:`~qsharp.estimator.EstimatorQecScheme` — custom model configuration. -- :class:`~qsharp.estimator.ErrorBudgetPartition` — budget and constraint settings. - -For full API documentation see [qsharp.estimator](:mod:`qsharp.estimator`). -""" - -from qsharp.estimator import * # pyright: ignore[reportWildcardImportFromLibrary] diff --git a/source/qdk_package/qdk/estimator/__init__.py b/source/qdk_package/qdk/estimator/__init__.py new file mode 100644 index 0000000000..ef870f3dad --- /dev/null +++ b/source/qdk_package/qdk/estimator/__init__.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from ._estimator import ( + EstimatorError, + LogicalCounts, + EstimatorResult, + QubitParams, + QECScheme, + MeasurementErrorRate, + EstimatorQubitParams, + EstimatorQecScheme, + ProtocolSpecificDistillationUnitSpecification, + DistillationUnitSpecification, + ErrorBudgetPartition, + EstimatorConstraints, + EstimatorInputParamsItem, + EstimatorParams, +) + +__all__ = [ + "EstimatorError", + "LogicalCounts", + "EstimatorResult", + "QubitParams", + "QECScheme", + "MeasurementErrorRate", + "EstimatorQubitParams", + "EstimatorQecScheme", + "ProtocolSpecificDistillationUnitSpecification", + "DistillationUnitSpecification", + "ErrorBudgetPartition", + "EstimatorConstraints", + "EstimatorInputParamsItem", + "EstimatorParams", +] diff --git a/source/pip/qsharp/estimator/_estimator.py b/source/qdk_package/qdk/estimator/_estimator.py similarity index 100% rename from source/pip/qsharp/estimator/_estimator.py rename to source/qdk_package/qdk/estimator/_estimator.py diff --git a/source/qdk_package/qdk/openqasm.py b/source/qdk_package/qdk/openqasm.py deleted file mode 100644 index c51382c4dc..0000000000 --- a/source/qdk_package/qdk/openqasm.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -"""OpenQASM interoperability for the Q# ecosystem. - -This module re-exports all public symbols from [qsharp.openqasm](:mod:`qsharp.openqasm`), -making them available under the ``qdk.openqasm`` namespace. It provides -functions for importing, compiling, running, and estimating resources for -OpenQASM 2.0 and 3.0 programs using the local Q# toolchain. - -Key exports: - -- :func:`~qsharp.openqasm.import_openqasm` — parse and import an OpenQASM program into the Q# interpreter. -- :func:`~qsharp.openqasm.run` — execute an OpenQASM program and return shot results. -- :func:`~qsharp.openqasm.estimate` — run the Microsoft Resource Estimator on an OpenQASM program. -- :func:`~qsharp.openqasm.circuit` — synthesize a circuit diagram from an OpenQASM program. - -For full API documentation see [qsharp.openqasm](:mod:`qsharp.openqasm`). -""" - -from qsharp.openqasm import * # pyright: ignore[reportWildcardImportFromLibrary] diff --git a/source/qdk_package/qdk/openqasm/__init__.py b/source/qdk_package/qdk/openqasm/__init__.py new file mode 100644 index 0000000000..5e9d3757a0 --- /dev/null +++ b/source/qdk_package/qdk/openqasm/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from ._circuit import circuit +from ._compile import compile +from ._estimate import estimate +from ._import import import_openqasm +from ._run import run +from .._native import ProgramType, OutputSemantics, QasmError # type: ignore + +__all__ = [ + "circuit", + "compile", + "estimate", + "import_openqasm", + "run", + "ProgramType", + "OutputSemantics", + "QasmError", +] diff --git a/source/pip/qsharp/openqasm/_circuit.py b/source/qdk_package/qdk/openqasm/_circuit.py similarity index 98% rename from source/pip/qsharp/openqasm/_circuit.py rename to source/qdk_package/qdk/openqasm/_circuit.py index eaed78cba8..8719c03a51 100644 --- a/source/pip/qsharp/openqasm/_circuit.py +++ b/source/qdk_package/qdk/openqasm/_circuit.py @@ -6,13 +6,12 @@ from .._fs import read_file, list_directory, resolve from .._http import fetch_github from .._native import circuit_qasm_program # type: ignore -from .._qsharp import ( +from .._interpreter import ( get_interpreter, ipython_helper, - Circuit, - CircuitConfig, python_args_to_interpreter_args, ) +from .._native import Circuit, CircuitConfig from .. import telemetry_events diff --git a/source/pip/qsharp/openqasm/_compile.py b/source/qdk_package/qdk/openqasm/_compile.py similarity index 97% rename from source/pip/qsharp/openqasm/_compile.py rename to source/qdk_package/qdk/openqasm/_compile.py index 8f34963eb1..7a514ec3e5 100644 --- a/source/pip/qsharp/openqasm/_compile.py +++ b/source/qdk_package/qdk/openqasm/_compile.py @@ -9,13 +9,13 @@ from .._native import ( # type: ignore compile_qasm_program_to_qir, ) -from .._qsharp import ( - QirInputData, +from .._types import QirInputData +from .._interpreter import ( get_interpreter, ipython_helper, - TargetProfile, python_args_to_interpreter_args, ) +from .._native import TargetProfile from .. import telemetry_events diff --git a/source/pip/qsharp/openqasm/_estimate.py b/source/qdk_package/qdk/openqasm/_estimate.py similarity index 99% rename from source/pip/qsharp/openqasm/_estimate.py rename to source/qdk_package/qdk/openqasm/_estimate.py index 7534562600..047eef5688 100644 --- a/source/pip/qsharp/openqasm/_estimate.py +++ b/source/qdk_package/qdk/openqasm/_estimate.py @@ -12,7 +12,7 @@ ) from ..estimator import EstimatorParams, EstimatorResult -from .._qsharp import ( +from .._interpreter import ( get_interpreter, ipython_helper, python_args_to_interpreter_args, diff --git a/source/pip/qsharp/openqasm/_import.py b/source/qdk_package/qdk/openqasm/_import.py similarity index 98% rename from source/pip/qsharp/openqasm/_import.py rename to source/qdk_package/qdk/openqasm/_import.py index e616ee0d39..d2e26c5616 100644 --- a/source/pip/qsharp/openqasm/_import.py +++ b/source/qdk_package/qdk/openqasm/_import.py @@ -7,7 +7,7 @@ from ._ipython import display_or_print from .._fs import read_file, list_directory, resolve from .._http import fetch_github -from .._qsharp import ( +from .._interpreter import ( get_interpreter, ipython_helper, ) diff --git a/source/pip/qsharp/openqasm/_ipython.py b/source/qdk_package/qdk/openqasm/_ipython.py similarity index 100% rename from source/pip/qsharp/openqasm/_ipython.py rename to source/qdk_package/qdk/openqasm/_ipython.py diff --git a/source/pip/qsharp/openqasm/_run.py b/source/qdk_package/qdk/openqasm/_run.py similarity index 98% rename from source/pip/qsharp/openqasm/_run.py rename to source/qdk_package/qdk/openqasm/_run.py index 1b82cb41ff..0401f0dad9 100644 --- a/source/pip/qsharp/openqasm/_run.py +++ b/source/qdk_package/qdk/openqasm/_run.py @@ -6,19 +6,20 @@ from .._fs import read_file, list_directory, resolve from .._http import fetch_github from .._native import QasmError, Output, run_qasm_program # type: ignore -from .._qsharp import ( +from .._types import ( BitFlipNoise, DepolarizingNoise, PauliNoise, PhaseFlipNoise, ShotResult, StateDump, - StateDumpData, +) +from .._interpreter import ( get_interpreter, ipython_helper, python_args_to_interpreter_args, - NoiseConfig, ) +from .._native import StateDumpData, NoiseConfig from .. import telemetry_events from ._ipython import display_or_print diff --git a/source/pip/qsharp/openqasm/_utils.py b/source/qdk_package/qdk/openqasm/_utils.py similarity index 100% rename from source/pip/qsharp/openqasm/_utils.py rename to source/qdk_package/qdk/openqasm/_utils.py diff --git a/source/qdk_package/qdk/qiskit.py b/source/qdk_package/qdk/qiskit.py deleted file mode 100644 index 8f26479eb2..0000000000 --- a/source/qdk_package/qdk/qiskit.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -"""Qiskit interoperability for the Q# ecosystem. - -This module re-exports all public symbols from [qsharp.interop.qiskit](:mod:`qsharp.interop.qiskit`), -making them available under the ``qdk.qiskit`` namespace. It provides Qiskit -backends backed by the local Q# simulator and NeutralAtomDevice, allowing -Qiskit circuits to be run locally without any cloud connection. - -Key exports: - -- :class:`~qsharp.interop.qiskit.backends.qsharp_backend.QSharpBackend` -- :class:`~qsharp.interop.qiskit.backends.neutral_atom_backend.NeutralAtomBackend` -- :class:`~qsharp.interop.qiskit.backends.re_backend.ResourceEstimatorBackend` -- :func:`~qsharp.interop.qiskit.estimate` - -For full API documentation see [qsharp.interop.qiskit](:mod:`qsharp.interop.qiskit`). - -Requires the ``qiskit`` extra: ``pip install qdk[qiskit]``. - -Usage: - - from qiskit import QuantumCircuit - from qdk.qiskit import NeutralAtomBackend - - circuit = QuantumCircuit(2, 2) - circuit.h(0) - circuit.cx(0, 1) - circuit.measure([0, 1], [0, 1]) - - backend = NeutralAtomBackend() - job = backend.run(circuit, shots=1000) - result = job.result() - print(result.results[0].data.counts) -""" - -try: - from qsharp.interop.qiskit import * # pyright: ignore[reportWildcardImportFromLibrary] -except Exception as ex: - raise ImportError( - "qdk.qiskit requires the qiskit extra. Install with 'pip install qdk[qiskit]'." - ) from ex diff --git a/source/qdk_package/qdk/qiskit/__init__.py b/source/qdk_package/qdk/qiskit/__init__.py new file mode 100644 index 0000000000..db8e28ae8f --- /dev/null +++ b/source/qdk_package/qdk/qiskit/__init__.py @@ -0,0 +1,110 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Qiskit interoperability for the Q# ecosystem. + +This module provides Qiskit backends backed by the local Q# simulator and +NeutralAtomDevice, allowing Qiskit circuits to be run locally without any +cloud connection. + +Available backends: + +- :class:`~qsharp.interop.qiskit.QSharpBackend` + Runs any Qiskit ``QuantumCircuit`` using the Q# simulator. Supports + noise-free simulation via QASM export and QIR compilation. + +- :class:`~qsharp.interop.qiskit.NeutralAtomBackend` + Runs Qiskit circuits on the local NeutralAtomDevice simulator. Decomposes + gates to the native ``{Rz, SX, CZ}`` gate set and optionally models + per-gate noise (including qubit loss). Loss shots are exposed separately + from accepted shots in the job result. + +- :class:`~qsharp.interop.qiskit.ResourceEstimatorBackend` + Estimates quantum resources (qubits, T-gates, etc.) for a Qiskit circuit + without running a full simulation. + +- :func:`~qsharp.interop.qiskit.estimate` + Convenience function that runs resource estimation on a Qiskit circuit + and returns an :class:`~qsharp.estimator.EstimatorResult` directly, without + needing to construct a backend or job manually. + +Usage: + + from qiskit import QuantumCircuit + from qsharp.interop.qiskit import NeutralAtomBackend + from qdk.simulation import NoiseConfig + + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure([0, 1], [0, 1]) + + noise = NoiseConfig() + noise.rz.loss = 0.05 # 5% qubit loss per Rz gate + + backend = NeutralAtomBackend() + job = backend.run(circuit, shots=1000, noise=noise, seed=42) + result = job.result() + print(result.results[0].data.counts) # accepted shots only + print(result.results[0].data.raw_counts) # includes loss shots +""" + +from typing import Any, Dict, List, Optional, Union + +from ..estimator import EstimatorParams, EstimatorResult +from .._native import OutputSemantics, ProgramType, QasmError +from .backends import ( + NeutralAtomBackend, + QSharpBackend, + ResourceEstimatorBackend, + QirTarget, +) +from .jobs import QsJob, QsSimJob, ReJob, QsJobSet +from .execution import DetaultExecutor +from qiskit import QuantumCircuit + + +def estimate( + circuit: QuantumCircuit, + params: Optional[Union[Dict[str, Any], List, EstimatorParams]] = None, + **options, +) -> EstimatorResult: + """ + Estimates resources for Qiskit QuantumCircuit. + + :param circuit: The input Qiskit QuantumCircuit object. + :param params: The parameters to configure physical estimation. + :type params: EstimatorParams or dict or list + :param **options: Additional options for the transpiler, exporter, or Qiskit passes + configuration. Defaults to backend config values. Common options: + + - ``optimization_level`` (int): Transpiler optimization level. + - ``basis_gates`` (list): Basis gates for transpilation. + - ``includes`` (list): Include paths for QASM resolution. + - ``search_path`` (str): Search path for resolving file references. + :raises QasmError: If there is an error generating or parsing QASM. + :return: The estimated resources. + :rtype: EstimatorResult + """ + from .._interpreter import ipython_helper + + ipython_helper() + backend = ResourceEstimatorBackend() + job = backend.run(circuit, params=params, **options) + return job.result() + + +__all__ = [ + "NeutralAtomBackend", + "QSharpBackend", + "ResourceEstimatorBackend", + "QirTarget", + "QsJob", + "QsSimJob", + "ReJob", + "QsJobSet", + "estimate", + "EstimatorParams", + "EstimatorResult", + "QasmError", +] diff --git a/source/pip/qsharp/interop/qiskit/backends/__init__.py b/source/qdk_package/qdk/qiskit/backends/__init__.py similarity index 100% rename from source/pip/qsharp/interop/qiskit/backends/__init__.py rename to source/qdk_package/qdk/qiskit/backends/__init__.py diff --git a/source/pip/qsharp/interop/qiskit/backends/backend_base.py b/source/qdk_package/qdk/qiskit/backends/backend_base.py similarity index 98% rename from source/pip/qsharp/interop/qiskit/backends/backend_base.py rename to source/qdk_package/qdk/qiskit/backends/backend_base.py index 8d24a45499..ea2d401f01 100644 --- a/source/pip/qsharp/interop/qiskit/backends/backend_base.py +++ b/source/qdk_package/qdk/qiskit/backends/backend_base.py @@ -27,7 +27,7 @@ from ..execution import DetaultExecutor from ..jobs import QsJob, QsSimJob, QsJobSet from ..passes import RemoveDelays -from .... import TargetProfile +from ... import TargetProfile logger = logging.getLogger(__name__) @@ -271,7 +271,7 @@ def run_job( logger.error("%s: run failed.", self.name) if output: logger.error("Output: %s", output) - from .... import QSharpError + from ... import QSharpError raise QSharpError(str(Errors.RUN_TERMINATED_WITHOUT_OUTPUT)) @@ -356,7 +356,7 @@ def _map_result_bit(self, v) -> str: to emit a loss marker instead of the default string fallback for unknown values. """ - from .... import Result as QSharpResult + from ... import Result as QSharpResult if v == QSharpResult.One: return "1" @@ -582,9 +582,9 @@ def _qasm_to_qir( source: str, **kwargs, ) -> str: - from ...._native import compile_qasm_program_to_qir - from ...._fs import read_file, list_directory, resolve - from ...._http import fetch_github + from ..._native import compile_qasm_program_to_qir + from ..._fs import read_file, list_directory, resolve + from ..._http import fetch_github return compile_qasm_program_to_qir( source, @@ -600,9 +600,9 @@ def _qasm_to_qsharp( source: str, **kwargs, ) -> str: - from ...._native import compile_qasm_to_qsharp - from ...._fs import read_file, list_directory, resolve - from ...._http import fetch_github + from ..._native import compile_qasm_to_qsharp + from ..._fs import read_file, list_directory, resolve + from ..._http import fetch_github return compile_qasm_to_qsharp( source, diff --git a/source/pip/qsharp/interop/qiskit/backends/compilation.py b/source/qdk_package/qdk/qiskit/backends/compilation.py similarity index 100% rename from source/pip/qsharp/interop/qiskit/backends/compilation.py rename to source/qdk_package/qdk/qiskit/backends/compilation.py diff --git a/source/pip/qsharp/interop/qiskit/backends/errors.py b/source/qdk_package/qdk/qiskit/backends/errors.py similarity index 100% rename from source/pip/qsharp/interop/qiskit/backends/errors.py rename to source/qdk_package/qdk/qiskit/backends/errors.py diff --git a/source/pip/qsharp/interop/qiskit/backends/neutral_atom_backend.py b/source/qdk_package/qdk/qiskit/backends/neutral_atom_backend.py similarity index 98% rename from source/pip/qsharp/interop/qiskit/backends/neutral_atom_backend.py rename to source/qdk_package/qdk/qiskit/backends/neutral_atom_backend.py index e78ac94d46..77667b369f 100644 --- a/source/pip/qsharp/interop/qiskit/backends/neutral_atom_backend.py +++ b/source/qdk_package/qdk/qiskit/backends/neutral_atom_backend.py @@ -9,7 +9,7 @@ from qiskit.providers import Options from qiskit.transpiler.target import Target -from .... import Result, TargetProfile +from ... import Result, TargetProfile from .. import OutputSemantics from ..execution import DetaultExecutor from ..jobs import QsSimJob, QsJobSet @@ -50,7 +50,7 @@ class NeutralAtomBackend(BackendBase): from qiskit import QuantumCircuit from qsharp.interop.qiskit import NeutralAtomBackend - from qsharp._simulation import NoiseConfig + from qdk.simulation import NoiseConfig qc = QuantumCircuit(2) qc.h(0) @@ -124,7 +124,7 @@ def __init__( def _get_device(self): """Return the NeutralAtomDevice, creating a default one on first access.""" if self._device is None: - from qsharp._device._atom import NeutralAtomDevice + from ..._device._atom import NeutralAtomDevice self._device = NeutralAtomDevice() return self._device diff --git a/source/pip/qsharp/interop/qiskit/backends/neutral_atom_target.py b/source/qdk_package/qdk/qiskit/backends/neutral_atom_target.py similarity index 100% rename from source/pip/qsharp/interop/qiskit/backends/neutral_atom_target.py rename to source/qdk_package/qdk/qiskit/backends/neutral_atom_target.py diff --git a/source/pip/qsharp/interop/qiskit/backends/qirtarget.py b/source/qdk_package/qdk/qiskit/backends/qirtarget.py similarity index 99% rename from source/pip/qsharp/interop/qiskit/backends/qirtarget.py rename to source/qdk_package/qdk/qiskit/backends/qirtarget.py index b88f2056a1..340ed7152f 100644 --- a/source/pip/qsharp/interop/qiskit/backends/qirtarget.py +++ b/source/qdk_package/qdk/qiskit/backends/qirtarget.py @@ -48,7 +48,7 @@ ) from qiskit.transpiler.target import Target -from .... import TargetProfile +from ... import TargetProfile logger = logging.getLogger(__name__) diff --git a/source/pip/qsharp/interop/qiskit/backends/qsharp_backend.py b/source/qdk_package/qdk/qiskit/backends/qsharp_backend.py similarity index 97% rename from source/pip/qsharp/interop/qiskit/backends/qsharp_backend.py rename to source/qdk_package/qdk/qiskit/backends/qsharp_backend.py index c23db5d849..e7ebf589c7 100644 --- a/source/pip/qsharp/interop/qiskit/backends/qsharp_backend.py +++ b/source/qdk_package/qdk/qiskit/backends/qsharp_backend.py @@ -9,7 +9,7 @@ from qiskit import QuantumCircuit from qiskit.providers import Options from qiskit.transpiler.target import Target -from .... import TargetProfile +from ... import TargetProfile from .. import OutputSemantics from ..execution import DetaultExecutor from ..jobs import QsSimJob @@ -189,9 +189,9 @@ def _run_qasm( :raises QasmError: If there is an error generating, parsing, or compiling QASM. """ - from ...._native import run_qasm_program, Output # type: ignore - from ...._fs import read_file, list_directory, resolve - from ...._http import fetch_github + from ..._native import run_qasm_program, Output # type: ignore + from ..._fs import read_file, list_directory, resolve + from ..._http import fetch_github def callback(output: Output) -> None: print(output) diff --git a/source/pip/qsharp/interop/qiskit/backends/re_backend.py b/source/qdk_package/qdk/qiskit/backends/re_backend.py similarity index 97% rename from source/pip/qsharp/interop/qiskit/backends/re_backend.py rename to source/qdk_package/qdk/qiskit/backends/re_backend.py index 568de8c7a9..b7e9da9276 100644 --- a/source/pip/qsharp/interop/qiskit/backends/re_backend.py +++ b/source/qdk_package/qdk/qiskit/backends/re_backend.py @@ -18,11 +18,11 @@ from .. import OutputSemantics from ..jobs import ReJob from ..execution import DetaultExecutor -from ...._fs import read_file, list_directory, resolve -from ...._http import fetch_github -from ...._native import resource_estimate_qasm_program -from .... import TargetProfile -from ....estimator import ( +from ..._fs import read_file, list_directory, resolve +from ..._http import fetch_github +from ..._native import resource_estimate_qasm_program +from ... import TargetProfile +from ...estimator import ( EstimatorResult, EstimatorParams, ) diff --git a/source/pip/qsharp/interop/qiskit/execution/__init__.py b/source/qdk_package/qdk/qiskit/execution/__init__.py similarity index 100% rename from source/pip/qsharp/interop/qiskit/execution/__init__.py rename to source/qdk_package/qdk/qiskit/execution/__init__.py diff --git a/source/pip/qsharp/interop/qiskit/execution/default.py b/source/qdk_package/qdk/qiskit/execution/default.py similarity index 100% rename from source/pip/qsharp/interop/qiskit/execution/default.py rename to source/qdk_package/qdk/qiskit/execution/default.py diff --git a/source/pip/qsharp/interop/qiskit/jobs/__init__.py b/source/qdk_package/qdk/qiskit/jobs/__init__.py similarity index 100% rename from source/pip/qsharp/interop/qiskit/jobs/__init__.py rename to source/qdk_package/qdk/qiskit/jobs/__init__.py diff --git a/source/pip/qsharp/interop/qiskit/jobs/qsjob.py b/source/qdk_package/qdk/qiskit/jobs/qsjob.py similarity index 98% rename from source/pip/qsharp/interop/qiskit/jobs/qsjob.py rename to source/qdk_package/qdk/qiskit/jobs/qsjob.py index 23c3f27281..a5c1743b24 100644 --- a/source/pip/qsharp/interop/qiskit/jobs/qsjob.py +++ b/source/qdk_package/qdk/qiskit/jobs/qsjob.py @@ -16,8 +16,8 @@ from qiskit.providers import JobV1, JobStatus, JobError from ..execution import DetaultExecutor -from .... import telemetry_events -from ....estimator import EstimatorResult +from ... import telemetry_events +from ...estimator import EstimatorResult logger = logging.getLogger(__name__) diff --git a/source/pip/qsharp/interop/qiskit/jobs/qsjobset.py b/source/qdk_package/qdk/qiskit/jobs/qsjobset.py similarity index 99% rename from source/pip/qsharp/interop/qiskit/jobs/qsjobset.py rename to source/qdk_package/qdk/qiskit/jobs/qsjobset.py index 0a20908079..03956a1f5b 100644 --- a/source/pip/qsharp/interop/qiskit/jobs/qsjobset.py +++ b/source/qdk_package/qdk/qiskit/jobs/qsjobset.py @@ -18,7 +18,7 @@ from .qsjob import QsSimJob, RunInputCallable from ..execution import DetaultExecutor -from .... import telemetry_events +from ... import telemetry_events logger = logging.getLogger(__name__) diff --git a/source/pip/qsharp/interop/qiskit/passes/__init__.py b/source/qdk_package/qdk/qiskit/passes/__init__.py similarity index 100% rename from source/pip/qsharp/interop/qiskit/passes/__init__.py rename to source/qdk_package/qdk/qiskit/passes/__init__.py diff --git a/source/pip/qsharp/interop/qiskit/passes/remove_delay.py b/source/qdk_package/qdk/qiskit/passes/remove_delay.py similarity index 100% rename from source/pip/qsharp/interop/qiskit/passes/remove_delay.py rename to source/qdk_package/qdk/qiskit/passes/remove_delay.py diff --git a/source/qdk_package/qdk/qre/__init__.py b/source/qdk_package/qdk/qre/__init__.py index 5d0695bb9f..0dbe8d4a9d 100644 --- a/source/qdk_package/qdk/qre/__init__.py +++ b/source/qdk_package/qdk/qre/__init__.py @@ -1,28 +1,86 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# flake8: noqa F403 -# pyright: ignore[reportWildcardImportFromLibrary] +from ._application import Application +from ._architecture import Architecture, ISAContext +from ._estimation import estimate +from ._instruction import ( + LOGICAL, + PHYSICAL, + Encoding, + ISATransform, + constraint, + InstructionSource, +) +from ._isa_enumeration import ISAQuery, ISARefNode, ISA_ROOT +from ._qre import ( + ISA, + InstructionFrontier, + Constraint, + ConstraintBound, + EstimationResult, + FactoryResult, + ISARequirements, + Block, + Trace, + block_linear_function, + constant_function, + generic_function, + linear_function, + instruction_name, + property_name, + property_name_to_key, +) +from ._results import ( + EstimationTable, + EstimationTableColumn, + EstimationTableEntry, + plot_estimates, +) +from ._trace import LatticeSurgery, PSSPC, TraceQuery, TraceTransform -"""Quantum Resource Estimator (QRE) for the Q# ecosystem. +# Extend Rust Python types with additional Python-side functionality +from ._instruction import _isa_as_frame, _requirements_as_frame -This module re-exports all public symbols from [qsharp.qre](:mod:`qsharp.qre`), -making them available under the ``qdk.qre`` namespace. It provides tools for -estimating the resources required to run quantum applications on specific -hardware architectures. +ISA.as_frame = _isa_as_frame +ISARequirements.as_frame = _requirements_as_frame -Example: - - from qdk import qre - results = qre.estimate(app, arch, isa_query) - -Requires the ``qre`` extra: ``pip install qdk[qre]``. -""" - -try: - # Re-export the top-level qsharp.qre names. - from qsharp.qre import * -except Exception as ex: - raise ImportError( - "qdk.qre requires the qre extra. Install with 'pip install qdk[qre]'." - ) from ex +__all__ = [ + "block_linear_function", + "constant_function", + "constraint", + "estimate", + "linear_function", + "plot_estimates", + "Application", + "Architecture", + "Block", + "Constraint", + "ConstraintBound", + "Encoding", + "EstimationResult", + "EstimationTable", + "EstimationTableColumn", + "EstimationTableEntry", + "FactoryResult", + "generic_function", + "instruction_name", + "InstructionFrontier", + "InstructionSource", + "ISA", + "ISA_ROOT", + "ISAContext", + "ISAQuery", + "ISARefNode", + "ISARequirements", + "ISATransform", + "LatticeSurgery", + "PSSPC", + "property_name", + "property_name_to_key", + "Trace", + "TraceQuery", + "TraceTransform", + "LOGICAL", + "PHYSICAL", +] diff --git a/source/pip/qsharp/qre/_application.py b/source/qdk_package/qdk/qre/_application.py similarity index 100% rename from source/pip/qsharp/qre/_application.py rename to source/qdk_package/qdk/qre/_application.py diff --git a/source/qdk_package/qdk/qre/_architecture.py b/source/qdk_package/qdk/qre/_architecture.py new file mode 100644 index 0000000000..cd8bb52e64 --- /dev/null +++ b/source/qdk_package/qdk/qre/_architecture.py @@ -0,0 +1,244 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from __future__ import annotations +import copy +from typing import cast, TYPE_CHECKING + +from abc import ABC, abstractmethod + +from ._qre import ( + ISA, + _ProvenanceGraph, + Instruction, + _IntFunction, + _FloatFunction, + constant_function, + property_name_to_key, +) + +if TYPE_CHECKING: + from typing import Optional + + from ._instruction import ISATransform, Encoding + + +class Architecture(ABC): + """Abstract base class for quantum hardware architectures.""" + + @abstractmethod + def provided_isa(self, ctx: ISAContext) -> ISA: + """ + Create the ISA provided by this architecture, adding instructions + directly to the context's provenance graph. + + Args: + ctx (ISAContext): The enumeration context whose provenance graph stores + the instructions. + + Returns: + ISA: The ISA backed by the context's provenance graph. + """ + ... + + def context(self) -> ISAContext: + """Create a new enumeration context for this architecture. + + Returns: + ISAContext: A new enumeration context. + """ + return ISAContext(self) + + +class ISAContext: + """ + Context passed through enumeration, holding shared state. + """ + + def __init__(self, arch: Architecture): + """Initialize the ISA context for the given architecture. + + Args: + arch (Architecture): The architecture providing the base ISA. + """ + self._provenance: _ProvenanceGraph = _ProvenanceGraph() + + # Let the architecture create instructions directly in the graph. + self._isa = arch.provided_isa(self) + + self._bindings: dict[str, ISA] = {} + self._transforms: dict[int, Architecture | ISATransform] = {0: arch} + + def _with_binding(self, name: str, isa: ISA) -> ISAContext: + """Return a new context with an additional binding (internal use).""" + ctx = copy.copy(self) + ctx._bindings = {**self._bindings, name: isa} + return ctx + + @property + def isa(self) -> ISA: + """The ISA provided by the architecture for this context.""" + return self._isa + + def add_instruction( + self, + id_or_instruction: int | Instruction, + encoding: Encoding = 0, # type: ignore + *, + arity: Optional[int] = 1, + time: int | _IntFunction = 0, + space: Optional[int] | _IntFunction = None, + length: Optional[int | _IntFunction] = None, + error_rate: float | _FloatFunction = 0.0, + transform: ISATransform | None = None, + source: list[Instruction] | None = None, + **kwargs: int, + ) -> int: + """ + Create an instruction and add it to the provenance graph. + + Can be called in two ways: + + 1. With keyword args to create a new instruction:: + + ctx.add_instruction(T, encoding=LOGICAL, time=1000, + error_rate=1e-8) + + 2. With a pre-existing ``Instruction`` object (e.g. from + ``with_id()``):: + + ctx.add_instruction(existing_instruction) + + Provenance is recorded when *transform* and/or *source* are + supplied: + + - **transform** — the ``ISATransform`` that produced the + instruction. + - **source** — input instructions consumed by the transform. + + Args: + id_or_instruction: Either an instruction ID (int) for creating + a new instruction, or an existing ``Instruction`` object. + encoding: The instruction encoding (0 = Physical, 1 = Logical). + Ignored when passing an existing ``Instruction``. + arity: The instruction arity. ``None`` for variable arity. + Ignored when passing an existing ``Instruction``. + time: Instruction time in ns (or ``_IntFunction`` for variable + arity). Ignored when passing an existing ``Instruction``. + space: Instruction space in physical qubits (or ``_IntFunction`` + for variable arity). Ignored when passing an existing + ``Instruction``. + length: Arity including ancilla qubits. Ignored when passing an + existing ``Instruction``. + error_rate: Instruction error rate (or ``_FloatFunction`` for + variable arity). Ignored when passing an existing + ``Instruction``. + transform: The ``ISATransform`` that produced the instruction. + source: List of source ``Instruction`` objects consumed by the + transform. + **kwargs: Additional properties (e.g. ``distance=9``). Ignored + when passing an existing ``Instruction``. + + Returns: + The node index in the provenance graph. + + Raises: + ValueError: If an unknown property name is provided in kwargs. + """ + if transform is None and source is None: + return self._provenance.add_instruction( + cast(int, id_or_instruction), + encoding, + arity=arity, + time=time, + space=space, + length=length, + error_rate=error_rate, + **kwargs, + ) + + if isinstance(id_or_instruction, Instruction): + instr = id_or_instruction + else: + instr = _make_instruction( + id_or_instruction, + int(encoding), + arity, + time, + space, + length, + error_rate, + kwargs, + ) + + transform_id = id(transform) if transform is not None else 0 + children = [inst.source for inst in source] if source else [] + + node_index = self._provenance.add_node(instr, transform_id, children) + + if transform is not None: + self._transforms[transform_id] = transform + + return node_index + + def make_isa(self, *node_indices: int) -> ISA: + """ + Create an ISA backed by this context's provenance graph from the + given node indices. + + Args: + *node_indices (int): Node indices in the provenance graph. + + Returns: + ISA: An ISA referencing the provenance graph. + """ + return self._provenance.make_isa(list(node_indices)) + + +def _make_instruction( + id: int, + encoding: int, + arity: int | None, + time: int | _IntFunction, + space: int | _IntFunction | None, + length: int | _IntFunction | None, + error_rate: float | _FloatFunction, + properties: dict[str, int], +) -> Instruction: + """Build an ``Instruction`` from keyword arguments.""" + if arity is not None: + instr = Instruction.fixed_arity( + id, + encoding, + arity, + cast(int, time), + cast(int | None, space), + cast(int | None, length), + cast(float, error_rate), + ) + else: + if isinstance(time, int): + time = constant_function(time) + if isinstance(space, int): + space = constant_function(space) + if isinstance(length, int): + length = constant_function(length) + if isinstance(error_rate, (int, float)): + error_rate = constant_function(float(error_rate)) + + instr = Instruction.variable_arity( + id, + encoding, + time, + cast(_IntFunction, space), + error_rate, + length, + ) + + for key, value in properties.items(): + prop_key = property_name_to_key(key) + if prop_key is None: + raise ValueError(f"Unknown property '{key}'.") + instr.set_property(prop_key, value) + + return instr diff --git a/source/qdk_package/qdk/qre/_enumeration.py b/source/qdk_package/qdk/qre/_enumeration.py new file mode 100644 index 0000000000..b01d706944 --- /dev/null +++ b/source/qdk_package/qdk/qre/_enumeration.py @@ -0,0 +1,242 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import types +from typing import ( + Generator, + Type, + TypeVar, + Literal, + Union, + cast, + get_args, + get_origin, + get_type_hints, +) +from dataclasses import MISSING +from itertools import product +from enum import Enum + + +T = TypeVar("T") + + +def _is_union_type(tp) -> bool: + """Check if a type is a Union or Python 3.10+ union (X | Y).""" + return get_origin(tp) is Union or isinstance(tp, types.UnionType) + + +def _is_type_filter(val, union_members: tuple) -> bool: + """ + Check if *val* is a union member type or a list of union member types, + i.e. a type filter for a union field (as opposed to a fixed value or + instance domain). + """ + member_set = set(union_members) + if isinstance(val, type) and val in member_set: + return True + if isinstance(val, list) and all( + isinstance(v, type) and v in member_set for v in val + ): + return True + return False + + +def _is_union_constraint_dict(val) -> bool: + """ + Check if *val* is a dict whose keys are all types, i.e. a per-member + constraint mapping for a union field. + + Example: ``{OptionA: {"number": [2, 3]}, OptionB: {}}`` + """ + return isinstance(val, dict) and all(isinstance(k, type) for k in val) + + +def _enumerate_union_members( + union_members: tuple, + val=None, +) -> list: + """ + Enumerate instances for a union-typed field. + + *val* controls which members are enumerated and how: + + - ``None`` - enumerate all members with their default domains. + - A single type (e.g. ``OptionB``) - enumerate only that member. + - A list of types (e.g. ``[OptionA, OptionB]``) - enumerate those members. + - A dict mapping types to constraint dicts + (e.g. ``{OptionA: {"number": [2, 3]}, OptionB: {}}``) - + enumerate only the listed members, forwarding the constraint dicts. + """ + # No override - enumerate all members with defaults + if val is None: + domain: list = [] + for member_type in union_members: + domain.extend(_enumerate_instances(member_type)) + return domain + + # Single type + if isinstance(val, type): + return list(_enumerate_instances(val)) + + # List of types + if isinstance(val, list) and all(isinstance(v, type) for v in val): + domain = [] + for member_type in val: + domain.extend(_enumerate_instances(member_type)) + return domain + + # Dict of type → constraint dict + if _is_union_constraint_dict(val): + domain = [] + for member_type, member_kwargs in cast(dict, val).items(): + domain.extend(_enumerate_instances(member_type, **member_kwargs)) + return domain + + raise ValueError( + f"Invalid value for union field: {val!r}. " + "Expected a union member type, a list of types, or a dict mapping " + "types to constraint dicts." + ) + + +def _enumerate_instances(cls: Type[T], **kwargs) -> Generator[T, None, None]: + """ + Yield all instances of a dataclass given its class. + + The enumeration logic supports defining domains for fields using the + ``domain`` metadata key. Additionally, boolean fields are automatically + enumerated with ``[True, False]``, Enum fields with all their members, + and Literal types with their defined values. + + **Nested dataclass fields** can be constrained by passing a dict:: + + _enumerate_instances(Outer, inner={"option": True}) + + **Union-typed fields** support several override forms: + + - A single type to select one member:: + + _enumerate_instances(Config, option=OptionB) + + - A list of types to select a subset:: + + _enumerate_instances(Config, option=[OptionA, OptionB]) + + - A dict mapping types to constraint dicts:: + + _enumerate_instances(Config, option={OptionA: {"number": [2, 3]}, OptionB: {}}) + + Args: + cls (Type[T]): The dataclass type to enumerate. + **kwargs: Fixed values or domains for fields. If a value is a list + and the corresponding field is kw_only, it is treated as a domain + to enumerate over. For nested dataclass fields a ``dict`` value + is forwarded as keyword arguments. For union-typed fields a type, + list of types, or ``dict[type, dict]`` controls member selection + and constraints. + + Returns: + Generator[T, None, None]: A generator yielding instances of the + dataclass. + + Raises: + ValueError: If a field cannot be enumerated (no domain found). + """ + + names = [] + values = [] + fixed_kwargs = {} + + if (fields := getattr(cls, "__dataclass_fields__", None)) is None: + # There are no fields defined for this class, so just yield a single + # instance + yield cls(**kwargs) + return + + # Resolve type hints to handle stringified types from __future__.annotations + type_hints = get_type_hints(cls) + + for field in fields.values(): # type: ignore + name = field.name + # Get resolved type or fallback to field.type + current_type = type_hints.get(name, field.type) + + if name in kwargs: + val = kwargs[name] + + is_union = _is_union_type(current_type) + union_members = get_args(current_type) if is_union else () + + # Union field with a type filter or constraint dict + if is_union and ( + _is_type_filter(val, union_members) or _is_union_constraint_dict(val) + ): + names.append(name) + values.append(_enumerate_union_members(union_members, val)) + continue + + # Nested dataclass field with a dict of constraints + if ( + isinstance(val, dict) + and not is_union + and isinstance(current_type, type) + and hasattr(current_type, "__dataclass_fields__") + ): + names.append(name) + values.append(list(_enumerate_instances(current_type, **val))) + continue + + # If kw_only and list, it's a domain to enumerate + if field.kw_only and isinstance(val, list): + names.append(name) + values.append(val) + else: + # Otherwise, it's a fixed value + fixed_kwargs[name] = val + continue + + if not field.kw_only: + # We don't enumerate non-kw-only fields that aren't in kwargs + continue + + # Derived domain logic + names.append(name) + + domain = field.metadata.get("domain", None) + if domain is not None: + values.append(domain) + continue + + if current_type is bool: + values.append([True, False]) + continue + + if isinstance(current_type, type) and issubclass(current_type, Enum): + values.append(list(current_type)) + continue + + if get_origin(current_type) is Literal: + values.append(list(get_args(current_type))) + continue + + # Union types (e.g., OptionA | OptionB or Union[OptionA, OptionB]) + if _is_union_type(current_type): + values.append(_enumerate_union_members(get_args(current_type), None)) + continue + + # Nested dataclass types + if isinstance(current_type, type) and hasattr( + current_type, "__dataclass_fields__" + ): + values.append(list(_enumerate_instances(current_type))) + continue + + if field.default is not MISSING: + values.append([field.default]) + continue + + raise ValueError(f"Cannot enumerate field {name}.") + + for instance_values in product(*values): + yield cls(**fixed_kwargs, **dict(zip(names, instance_values))) diff --git a/source/qdk_package/qdk/qre/_estimation.py b/source/qdk_package/qdk/qre/_estimation.py new file mode 100644 index 0000000000..228e139ede --- /dev/null +++ b/source/qdk_package/qdk/qre/_estimation.py @@ -0,0 +1,218 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from __future__ import annotations + +from typing import cast, Optional, Any + +from .. import telemetry_events +from ._application import Application +from ._architecture import Architecture +from ._qre import ( + _estimate_parallel, + _estimate_with_graph, + _EstimationCollection, + Trace, +) +from ._trace import TraceQuery, PSSPC, LatticeSurgery +from ._isa_enumeration import ISAQuery +from ._results import EstimationTable, EstimationTableEntry + + +def estimate( + application: Application, + architecture: Architecture, + isa_query: ISAQuery, + trace_query: Optional[TraceQuery] = None, + *, + max_error: float = 1.0, + post_process: bool = False, + use_graph: bool = True, + name: Optional[str] = None, +) -> EstimationTable: + """ + Estimate the resource requirements for a given application instance and + architecture. + + The application instance might return multiple traces. Each of the traces + is transformed by the trace query, which applies several trace transforms in + sequence. Each transform may return multiple traces. Similarly, the + architecture's ISA is transformed by the ISA query, which applies several + ISA transforms in sequence, each of which may return multiple ISAs. The + estimation is performed for each combination of transformed trace and ISA. + The results are collected into an EstimationTable and returned. + + The collection only contains the results that are optimal with respect to + the total number of qubits and the total runtime. + + Note: + The pruning strategy used when ``use_graph`` is set to True (default) + filters ISA instructions by comparing their per-instruction space, time, + and error independently. However, the total qubit count of a result + depends on the interaction between factory space and runtime: + ``factory_qubits = copies × factory_space`` where copies are determined + by ``count.div_ceil(runtime / factory_time)``. Because of this, an ISA + instruction that is dominated on per-instruction metrics can still + contribute to a globally Pareto-optimal result (e.g., a factory with + higher time may need fewer copies, leading to fewer total qubits). As a + consequence, ``use_graph=True`` may miss some results that + ``use_graph=False`` would find. Use ``use_graph=False`` when completeness of + the Pareto frontier is required. + + Args: + application (Application): The quantum application to be estimated. + architecture (Architecture): The target quantum architecture. + isa_query (ISAQuery): The ISA query to enumerate ISAs from the architecture. + trace_query (TraceQuery): The trace query to enumerate traces from the + application. + max_error (float): The maximum allowed error for the estimation results. + post_process (bool): If True, use the Python-threaded estimation path + (intended for future post-processing logic). If False (default), + use the Rust parallel estimation path. + use_graph (bool): If True (default), use the Rust estimation path that + builds a graph of ISAs and prunes suboptimal ISAs during estimation. + If False, use the Rust estimation path that does not perform any + pruning and simply enumerates all ISAs for each trace. + name (Optional[str]): An optional name for the estimation. If given, this + will be added as a first column to the results table for all entries. + + Returns: + EstimationTable: A table containing the optimal estimation results. + """ + + telemetry_events.on_qre_estimate(post_process=post_process, use_graph=use_graph) + + app_ctx = application.context() + arch_ctx = architecture.context() + + if trace_query is None: + trace_query = PSSPC.q() * LatticeSurgery.q() + + if post_process: + # Enumerate traces with their parameters so we can post-process later + params_and_traces = cast( + list[tuple[Any, Trace]], + list(trace_query.enumerate(app_ctx, track_parameters=True)), + ) + num_traces = len(params_and_traces) + + # Phase 1: Run all estimates in Rust (parallel, fast). + traces_only = [trace for _, trace in params_and_traces] + + if use_graph: + isa_query.populate(arch_ctx) + arch_ctx._provenance.build_pareto_index() + + num_isas = arch_ctx._provenance.total_isa_count() + + collection = _estimate_with_graph( + cast(list[Trace], traces_only), arch_ctx._provenance, max_error, True + ) + isas = collection.isas + else: + isas = list(isa_query.enumerate(arch_ctx)) + + num_isas = len(isas) + + collection = _estimate_parallel( + cast(list[Trace], traces_only), isas, max_error, True + ) + + total_jobs = collection.total_jobs + successful = collection.successful_estimates + summaries = collection.all_summaries # (trace_idx, isa_idx, qubits, runtime) + + # Phase 2: Learn per-trace runtime multiplier and qubit multiplier from + # one sample each: if post_process changes runtime or qubit count it + # will affect the Pareto optimality, but the changes depend only on the + # trace, not on the ISA. + trace_multipliers: dict[int, tuple[float, float]] = {} + trace_sample_isa: dict[int, int] = {} + for t_idx, isa_idx, _q, r in summaries: + if t_idx not in trace_sample_isa: + trace_sample_isa[t_idx] = isa_idx + for t_idx, isa_idx in trace_sample_isa.items(): + params, trace = params_and_traces[t_idx] + sample = trace.estimate(isas[isa_idx], max_error) + if sample is not None: + pre_q = sample.qubits + pre_r = sample.runtime + pp = app_ctx.application.post_process(params, sample) + if pp is not None and pre_r > 0 and pre_q > 0: + trace_multipliers[t_idx] = (pp.qubits / pre_q, pp.runtime / pre_r) + + # Phase 3: Estimate post-pp values and filter to Pareto candidates. + estimated_pp: list[tuple[int, int, int, int]] = ( + [] + ) # (t_idx, isa_idx, est_q, est_r) + for t_idx, isa_idx, q, r in summaries: + mult_q, mult_r = trace_multipliers.get(t_idx, (0.0, 0.0)) + est_q = int(q * mult_q) if mult_q > 0 else q + est_r = int(r * mult_r) if mult_r > 0 else r + estimated_pp.append((t_idx, isa_idx, est_q, est_r)) + + # Build approximate post-pp Pareto frontier to identify candidates. + estimated_pp.sort(key=lambda x: (x[2], x[3])) # sort by qubits, then runtime + approx_pareto: list[tuple[int, int, int, int]] = [] + min_r = float("inf") + for item in estimated_pp: + if item[3] < min_r: + approx_pareto.append(item) + min_r = item[3] + + # Phase 4: Re-estimate and post-process only the Pareto candidates. + pp_collection = _EstimationCollection() + for t_idx, isa_idx, _q, _r in approx_pareto: + params, trace = params_and_traces[t_idx] + result = trace.estimate(isas[isa_idx], max_error) + if result is not None: + pp_result = app_ctx.application.post_process(params, result) + if pp_result is not None: + pp_collection.insert(pp_result) + collection = pp_collection + else: + traces = list(trace_query.enumerate(app_ctx)) + num_traces = len(traces) + + if use_graph: + isa_query.populate(arch_ctx) + arch_ctx._provenance.build_pareto_index() + + num_isas = arch_ctx._provenance.total_isa_count() + + collection = _estimate_with_graph( + cast(list[Trace], traces), arch_ctx._provenance, max_error, False + ) + else: + isas = list(isa_query.enumerate(arch_ctx)) + + num_isas = len(isas) + + # Use the Rust parallel estimation path + collection = _estimate_parallel( + cast(list[Trace], traces), isas, max_error, False + ) + + total_jobs = collection.total_jobs + successful = collection.successful_estimates + + # Post-process the results and add them to a results table + table = EstimationTable() + + table.name = name + + if name is not None: + table.insert_column(0, "name", lambda entry: name) + + table.extend( + EstimationTableEntry.from_result(result, arch_ctx) for result in collection + ) + + # Fill in the stats for this estimation run + table.stats.num_traces = num_traces + table.stats.num_isas = num_isas + table.stats.total_jobs = total_jobs + table.stats.successful_estimates = successful + table.stats.pareto_results = len(collection) + + return table diff --git a/source/qdk_package/qdk/qre/_instruction.py b/source/qdk_package/qdk/qre/_instruction.py new file mode 100644 index 0000000000..4669a86d4c --- /dev/null +++ b/source/qdk_package/qdk/qre/_instruction.py @@ -0,0 +1,473 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Generator, Iterable, Optional +from enum import IntEnum + +import pandas as pd + +from ._architecture import ISAContext, Architecture +from ._enumeration import _enumerate_instances +from ._isa_enumeration import ( + ISA_ROOT, + _BindingNode, + _ComponentQuery, + ISAQuery, +) +from ._qre import ( + ISA, + Constraint, + ConstraintBound, + Instruction, + ISARequirements, + instruction_name, + property_name_to_key, +) + + +class Encoding(IntEnum): + PHYSICAL = 0 + LOGICAL = 1 + + +PHYSICAL = Encoding.PHYSICAL +LOGICAL = Encoding.LOGICAL + + +def constraint( + id: int, + encoding: Encoding = PHYSICAL, + *, + arity: Optional[int] = 1, + error_rate: Optional[ConstraintBound] = None, + **kwargs: bool, +) -> Constraint: + """ + Create an instruction constraint. + + Args: + id (int): The instruction ID. + encoding (Encoding): The instruction encoding. PHYSICAL (0) or LOGICAL (1). + arity (Optional[int]): The instruction arity. If None, instruction is + assumed to have variable arity. Default is 1. + error_rate (Optional[ConstraintBound]): The constraint on the error rate. + **kwargs (bool): Required properties that matching instructions must have. + Valid property names: distance. Set to True to require the property. + + Returns: + Constraint: The instruction constraint. + + Raises: + ValueError: If an unknown property name is provided in kwargs. + """ + c = Constraint(id, encoding, arity, error_rate) + + for key, value in kwargs.items(): + if value: + if (prop_key := property_name_to_key(key)) is None: + raise ValueError(f"Unknown property '{key}'") + + c.add_property(prop_key) + + return c + + +class ISATransform(ABC): + """ + Abstract base class for transformations between ISAs (e.g., QEC schemes). + + An ISA transform defines a mapping from a required input ISA (e.g., + architecture constraints) to a provided output ISA (logical instructions). + It supports enumeration of configuration parameters. + """ + + @staticmethod + @abstractmethod + def required_isa() -> ISARequirements: + """ + Return the requirements that an implementation ISA must satisfy. + + Returns: + ISARequirements: The requirements for the underlying ISA. + """ + ... + + @abstractmethod + def provided_isa( + self, impl_isa: ISA, ctx: ISAContext + ) -> Generator[ISA, None, None]: + """ + Yields ISAs provided by this transform given an implementation ISA. + + Args: + impl_isa (ISA): The implementation ISA that satisfies requirements. + ctx (ISAContext): The enumeration context whose provenance graph + stores the instructions. + + Yields: + ISA: A provided logical ISA. + """ + ... + + @classmethod + def enumerate_isas( + cls, + impl_isa: ISA | Iterable[ISA], + ctx: ISAContext, + **kwargs, + ) -> Generator[ISA, None, None]: + """ + Enumerate all valid ISAs for this transform given implementation ISAs. + + This method iterates over all instances of the transform class (enumerating + hyperparameters) and filters implementation ISAs against requirements. + + Args: + impl_isa (ISA | Iterable[ISA]): One or more implementation ISAs. + ctx (ISAContext): The enumeration context. + **kwargs: Arguments passed to parameter enumeration. + + Yields: + ISA: Valid provided ISAs. + """ + isas = [impl_isa] if isinstance(impl_isa, ISA) else impl_isa + for isa in isas: + if not isa.satisfies(cls.required_isa()): + continue + + for component in _enumerate_instances(cls, **kwargs): + ctx._transforms[id(component)] = component + yield from component.provided_isa(isa, ctx) + + @classmethod + def q(cls, *, source: ISAQuery | None = None, **kwargs) -> ISAQuery: + """ + Create an ISAQuery node for this transform. + + Args: + source (Node | None): The source node providing implementation ISAs. + Defaults to ISA_ROOT. + **kwargs: Fixed values or domains for dataclass fields. Keyword-only + fields with a ``metadata["domain"]`` are enumerated automatically; + passing a value for such a field overrides or restricts the + domain. Non-keyword-only fields passed here are used as fixed + values for all enumerated instances. + + For example, given a transform with a non-keyword-only field + ``threshold`` and a keyword-only field ``distance`` with a + domain, calling ``MyTransform.q(threshold=0.03)`` fixes + ``threshold`` to 0.03 while still enumerating over all values + in the ``distance`` domain. + + Returns: + ISAQuery: An enumeration node representing this transform. + """ + return _ComponentQuery( + cls, source=source if source is not None else ISA_ROOT, kwargs=kwargs + ) + + @classmethod + def bind(cls, name: str, node: ISAQuery) -> _BindingNode: + """ + Create a BindingNode for this transform. + + This is a convenience method equivalent to ``cls.q().bind(name, node)``. + + Args: + name (str): The name to bind the transform's output to. + node (Node): The child node that can reference this binding. + + Returns: + BindingNode: A binding node enclosing this transform. + """ + return cls.q().bind(name, node) + + +@dataclass(slots=True) +class InstructionSource: + nodes: list[_InstructionSourceNode] = field(default_factory=list, init=False) + roots: list[int] = field(default_factory=list, init=False) + + @classmethod + def from_isa(cls, ctx: ISAContext, isa: ISA) -> InstructionSource: + """ + Construct an InstructionSource graph from an ISA. + + The instruction source graph contains more information than the + provenance graph in the context, as it connects the instructions to the + transforms and architectures that generated them. + + Args: + ctx (ISAContext): The enumeration context containing the provenance graph. + isa (ISA): Instructions in the ISA will serve as root nodes in the source graph. + + Returns: + InstructionSource: The instruction source graph for the estimation result. + """ + + def _make_node( + graph: InstructionSource, source_table: dict[int, int], source: int + ) -> int: + if source in source_table: + return source_table[source] + + children = [ + _make_node(graph, source_table, child) + for child in ctx._provenance.children(source) + if child != 0 + ] + + node = graph.add_node( + ctx._provenance.instruction(source), + ctx._transforms.get(ctx._provenance.transform_id(source)), + children, + ) + + source_table[source] = node + return node + + graph = cls() + source_table: dict[int, int] = {} + + for inst in isa: + node_idx = isa.node_index(inst.id) + if node_idx is not None and node_idx != 0: + node = _make_node(graph, source_table, node_idx) + graph.add_root(node) + + return graph + + def add_root(self, node_id: int) -> None: + """Add a root node to the instruction source graph. + + Args: + node_id (int): The index of the node to add as a root. + """ + self.roots.append(node_id) + + def add_node( + self, + instruction: Instruction, + transform: Optional[ISATransform | Architecture], + children: list[int], + ) -> int: + """Add a node to the instruction source graph. + + Args: + instruction (Instruction): The instruction for this node. + transform (Optional[ISATransform | Architecture]): The transform + that produced the instruction. + children (list[int]): Indices of child nodes. + + Returns: + int: The index of the newly added node. + """ + node_id = len(self.nodes) + self.nodes.append(_InstructionSourceNode(instruction, transform, children)) + return node_id + + def __str__(self) -> str: + """Return a formatted string representation of the instruction source graph.""" + + def _format_node(node: _InstructionSourceNode, indent: int = 0) -> str: + result = " " * indent + f"{instruction_name(node.instruction.id) or '??'}" + if node.transform is not None: + result += f" @ {node.transform}" + for child_index in node.children: + result += "\n" + _format_node(self.nodes[child_index], indent + 2) + return result + + return "\n".join( + _format_node(self.nodes[root_index]) for root_index in self.roots + ) + + def __getitem__(self, id: int) -> _InstructionSourceNodeReference: + """ + Retrieve the first instruction source root node with the given + instruction ID. Raises KeyError if no such node exists. + + Args: + id (int): The instruction ID to search for. + + Returns: + _InstructionSourceNodeReference: The first instruction source node with the + given instruction ID. + """ + if (node := self.get(id)) is not None: + return node + + raise KeyError(f"Instruction ID {id} not found in instruction source graph.") + + def __contains__(self, id: int) -> bool: + """ + Check if there is an instruction source root node with the given + instruction ID. + + Args: + id (int): The instruction ID to search for. + + Returns: + bool: True if a node with the given instruction ID exists, False otherwise. + """ + for root in self.roots: + if self.nodes[root].instruction.id == id: + return True + + return False + + def get( + self, id: int, default: Optional[_InstructionSourceNodeReference] = None + ) -> Optional[_InstructionSourceNodeReference]: + """ + Retrieve the first instruction source root node with the given + instruction ID. Returns default if no such node exists. + + Args: + id (int): The instruction ID to search for. + default (Optional[_InstructionSourceNodeReference]): The value to return if no + node with the given ID is found. Default is None. + + Returns: + Optional[_InstructionSourceNodeReference]: The first instruction source node with the + given instruction ID, or default if no such node exists. + """ + for root in self.roots: + if self.nodes[root].instruction.id == id: + return _InstructionSourceNodeReference(self, root) + + return default + + +@dataclass(frozen=True, slots=True) +class _InstructionSourceNode: + """A node in the instruction source graph.""" + + instruction: Instruction + transform: Optional[ISATransform | Architecture] + children: list[int] + + +class _InstructionSourceNodeReference: + """Reference to a node in an InstructionSource graph.""" + + def __init__(self, graph: InstructionSource, node_id: int): + """Initialize a reference to a node in the instruction source graph. + + Args: + graph (InstructionSource): The owning instruction source graph. + node_id (int): The index of the referenced node. + """ + self.graph = graph + self.node_id = node_id + + @property + def instruction(self) -> Instruction: + """The instruction at this node.""" + return self.graph.nodes[self.node_id].instruction + + @property + def transform(self) -> Optional[ISATransform | Architecture]: + """The transform that produced this node's instruction, if any.""" + return self.graph.nodes[self.node_id].transform + + def __str__(self) -> str: + """Return a string representation of the referenced node.""" + return str(self.graph.nodes[self.node_id]) + + def __getitem__(self, id: int) -> _InstructionSourceNodeReference: + """ + Retrieve the first child instruction source node with the given + instruction ID. Raises KeyError if no such node exists. + + Args: + id (int): The instruction ID to search for. + + Returns: + _InstructionSourceNodeReference: The first child instruction source node with the + given instruction ID. + """ + if (node := self.get(id)) is not None: + return node + + raise KeyError( + f"Instruction ID {id} not found in children of instruction {instruction_name(self.instruction.id) or '??'}." + ) + + def get( + self, id: int, default: Optional[_InstructionSourceNodeReference] = None + ) -> Optional[_InstructionSourceNodeReference]: + """ + Retrieve the first child instruction source node with the given + instruction ID. Returns default if no such node exists. + + Args: + id (int): The instruction ID to search for. + default (Optional[_InstructionSourceNodeReference]): The value to return if no + node with the given ID is found. Default is None. + + Returns: + Optional[_InstructionSourceNodeReference]: The first child instruction source + node with the given instruction ID, or default if no such node + exists. + """ + + for child_id in self.graph.nodes[self.node_id].children: + if self.graph.nodes[child_id].instruction.id == id: + return _InstructionSourceNodeReference(self.graph, child_id) + + return default + + +def _isa_as_frame(self: ISA) -> pd.DataFrame: + """Convert an ISA to a pandas DataFrame. + + Args: + self (ISA): The ISA to convert. + + Returns: + pd.DataFrame: A DataFrame with columns for id, encoding, arity, + space, time, and error. + """ + data = { + "id": [instruction_name(inst.id) for inst in self], + "encoding": [Encoding(inst.encoding).name for inst in self], + "arity": [inst.arity for inst in self], + "space": [ + inst.expect_space() if inst.arity is not None else None for inst in self + ], + "time": [ + inst.expect_time() if inst.arity is not None else None for inst in self + ], + "error": [ + inst.expect_error_rate() if inst.arity is not None else None + for inst in self + ], + } + + df = pd.DataFrame(data) + df.set_index("id", inplace=True) + return df + + +def _requirements_as_frame(self: ISARequirements) -> pd.DataFrame: + """Convert ISA requirements to a pandas DataFrame. + + Args: + self (ISARequirements): The requirements to convert. + + Returns: + pd.DataFrame: A DataFrame with columns for id, encoding, and arity. + """ + data = { + "id": [instruction_name(inst.id) for inst in self], + "encoding": [Encoding(inst.encoding).name for inst in self], + "arity": [inst.arity for inst in self], + } + + df = pd.DataFrame(data) + df.set_index("id", inplace=True) + return df diff --git a/source/qdk_package/qdk/qre/_isa_enumeration.py b/source/qdk_package/qdk/qre/_isa_enumeration.py new file mode 100644 index 0000000000..7543c071ed --- /dev/null +++ b/source/qdk_package/qdk/qre/_isa_enumeration.py @@ -0,0 +1,428 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from __future__ import annotations + +import functools +import itertools +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Generator + +from ._architecture import ISAContext +from ._enumeration import _enumerate_instances +from ._qre import ISA + + +class ISAQuery(ABC): + """ + Abstract base class for all nodes in the ISA enumeration tree. + + Enumeration nodes define the structure of the search space for ISAs starting + from architectures and modified by ISA transforms such as error correction + schemes. They can be composed using operators like ``+`` (sum) and ``*`` + (product) to build complex enumeration strategies. + """ + + @abstractmethod + def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: + """ + Yields all ISA instances represented by this enumeration node. + + Args: + ctx (ISAContext): The enumeration context containing shared state, + e.g., access to the underlying architecture. + + Yields: + ISA: A possible ISA that can be generated from this node. + """ + pass + + def populate(self, ctx: ISAContext) -> int: + """ + Populate the provenance graph with instructions from this node. + + Unlike ``enumerate``, this does not yield ISA objects. Each transform + queries the graph for Pareto-optimal instructions matching its + requirements, and adds produced instructions directly to the graph. + + Args: + ctx (ISAContext): The enumeration context whose provenance graph + will be populated. + + Returns: + int: The starting node index of the instructions contributed by + this subtree. Used by consumers to scope graph queries to only + see their source's nodes. + """ + # Default implementation: consume enumerate for its side effects + start = ctx._provenance.raw_node_count() + for _ in self.enumerate(ctx): + pass + return start + + def __add__(self, other: ISAQuery) -> _SumNode: + """ + Perform a union of two enumeration nodes. + + Enumerating the sum node yields all ISAs from this node, followed by all + ISAs from the other node. Duplicate ISAs may be produced if both nodes + yield the same ISA. + + Args: + other (Node): The other enumeration node. + + Returns: + SumNode: A node representing the union of both enumerations. + + Example: + + The following enumerates ISAs from both SurfaceCode and ColorCode: + + .. code-block:: python + for isa in SurfaceCode.q() + ColorCode.q(): + ... + """ + if isinstance(self, _SumNode) and isinstance(other, _SumNode): + sources = self.sources + other.sources + return _SumNode(sources) + elif isinstance(self, _SumNode): + sources = self.sources + [other] + return _SumNode(sources) + elif isinstance(other, _SumNode): + sources = [self] + other.sources + return _SumNode(sources) + else: + return _SumNode([self, other]) + + def __mul__(self, other: ISAQuery) -> _ProductNode: + """ + Perform the cross product of two enumeration nodes. + + Enumerating the product node yields ISAs resulting from the Cartesian + product of ISAs from both nodes. The ISAs are combined using + concatenation (logical union). This means that instructions in the + other enumeration node with the same ID as an instruction in this + enumeration node will overwrite the instruction from this node. + + Args: + other (Node): The other enumeration node. + + Returns: + ProductNode: A node representing the product of both enumerations. + + Example: + + The following enumerates ISAs formed by combining ISAs from a + surface code and a factory: + + .. code-block:: python + + for isa in SurfaceCode.q() * Factory.q(): + ... + """ + if isinstance(self, _ProductNode) and isinstance(other, _ProductNode): + sources = self.sources + other.sources + return _ProductNode(sources) + elif isinstance(self, _ProductNode): + sources = self.sources + [other] + return _ProductNode(sources) + elif isinstance(other, _ProductNode): + sources = [self] + other.sources + return _ProductNode(sources) + else: + return _ProductNode([self, other]) + + def bind(self, name: str, node: ISAQuery) -> "_BindingNode": + """Create a BindingNode with this node as the component. + + Args: + name: The name to bind the component to. + node: The child enumeration node that may contain ISARefNodes. + + Returns: + A BindingNode with self as the component. + + Example: + + .. code-block:: python + ExampleErrorCorrection.q().bind("c", ISARefNode("c") * ISARefNode("c")) + """ + return _BindingNode(name=name, component=self, node=node) + + +@dataclass +class RootNode(ISAQuery): + """ + Represents the architecture's base ISA. + Reads from the context instead of holding a reference. + """ + + def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: + """ + Yields the architecture ISA from the context. + + Args: + ctx (Context): The enumeration context. + + Yields: + ISA: The architecture's provided ISA, called root. + """ + yield ctx._isa + + def populate(self, ctx: ISAContext) -> int: + """Architecture ISA is already in the graph from ``ISAContext.__init__``. + + Returns: + int: 1, since architecture nodes start at index 1. + """ + return 1 + + +# Singleton instance for convenience +ISA_ROOT = RootNode() + + +@dataclass +class _ComponentQuery(ISAQuery): + """ + Query node that enumerates ISAs based on a component type and source. + + This node takes a component type (which must have an ``enumerate_isas`` class + method) and a source node. It enumerates the source node to get base ISAs, + and then calls ``enumerate_isas`` on the component type for each base ISA + to generate derived ISAs. + + Attributes: + component: The component type to query (e.g., a QEC code class). + source: The source node providing input ISAs (default: ISA_ROOT). + kwargs: Additional keyword arguments passed to ``enumerate_isas``. + """ + + component: type + source: ISAQuery = field(default_factory=lambda: ISA_ROOT) + kwargs: dict = field(default_factory=dict) + + def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: + """ + Yields ISAs generated by the component from source ISAs. + + Args: + ctx (Context): The enumeration context. + + Yields: + ISA: A generated ISA instance. + """ + for isa in self.source.enumerate(ctx): + yield from self.component.enumerate_isas(isa, ctx, **self.kwargs) + + def populate(self, ctx: ISAContext) -> int: + """ + Populate the graph by querying matching instructions. + + Runs the source first to ensure dependency instructions are in + the graph, then queries the graph for all instructions matching + this component's requirements within the source's node range. + For each matching ISA × each hyperparameter instance, calls + ``provided_isa`` to add new instructions to the graph. + + Returns: + int: The starting node index of this component's own additions. + """ + source_start = self.source.populate(ctx) + impl_isas = ctx._provenance.query_satisfying( + self.component.required_isa(), min_node_idx=source_start + ) + own_start = ctx._provenance.raw_node_count() + for instance in _enumerate_instances(self.component, **self.kwargs): + ctx._transforms[id(instance)] = instance + for impl_isa in impl_isas: + for _ in instance.provided_isa(impl_isa, ctx): + pass + return own_start + + +@dataclass +class _ProductNode(ISAQuery): + """ + Node representing the Cartesian product of multiple source nodes. + + Attributes: + sources: A list of source nodes to combine. + """ + + sources: list[ISAQuery] + + def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: + """ + Yields ISAs formed by combining ISAs from all source nodes. + + Args: + ctx (Context): The enumeration context. + + Yields: + ISA: A combined ISA instance. + """ + source_generators = [source.enumerate(ctx) for source in self.sources] + yield from ( + functools.reduce(lambda a, b: a + b, isa_tuple) + for isa_tuple in itertools.product(*source_generators) + ) + + def populate(self, ctx: ISAContext) -> int: + """Populate the graph from each source sequentially (no cross product). + + Returns: + int: The starting node index before any source populated. + """ + first = ctx._provenance.raw_node_count() + for source in self.sources: + source.populate(ctx) + return first + + +@dataclass +class _SumNode(ISAQuery): + """ + Node representing the union of multiple source nodes. + + Attributes: + sources: A list of source nodes to enumerate sequentially. + """ + + sources: list[ISAQuery] + + def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: + """ + Yields ISAs from each source node in sequence. + + Args: + ctx (Context): The enumeration context. + + Yields: + ISA: An ISA instance from one of the sources. + """ + for source in self.sources: + yield from source.enumerate(ctx) + + def populate(self, ctx: ISAContext) -> int: + """Populate the graph from each source sequentially. + + Returns: + int: The starting node index before any source populated. + """ + first = ctx._provenance.raw_node_count() + for source in self.sources: + source.populate(ctx) + return first + + +@dataclass +class ISARefNode(ISAQuery): + """ + A reference to a bound ISA in the enumeration context. + + This node looks up the binding from the context and yields the bound ISA. + + Args: + name: The name of the bound ISA to reference. + """ + + name: str + + def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: + """ + Yields the bound ISA from the context. + + Args: + ctx (Context): The enumeration context containing bindings. + + Yields: + ISA: The bound ISA. + + Raises: + ValueError: If the name is not bound in the context. + """ + if self.name not in ctx._bindings: + raise ValueError(f"Undefined component reference: '{self.name}'") + yield ctx._bindings[self.name] + + def populate(self, ctx: ISAContext) -> int: + """Instructions already in graph from the bound component. + + Returns: + int: 1, since bound component nodes start at index 1. + """ + return 1 + + +@dataclass +class _BindingNode(ISAQuery): + """ + Enumeration node that binds a component to a name. + + This node enables the as_/ref pattern where multiple positions in the + enumeration tree share the same component instance. The bound component + is enumerated once, and its value is shared across all ISARefNodes with + the same name via the context. + + For multiple bindings, nest BindingNode instances. + + Args: + name: The name to bind the component to. + component: An EnumerationNode (e.g., _ComponentQuery) that produces the bound ISAs. + node: The child enumeration node that may contain ISARefNodes. + + Example: + + .. code-block:: python + ctx = EnumerationContext(architecture=arch) + + # Bind a code and reference it multiple times + BindingNode( + name="c", + component=ExampleErrorCorrection.q(), + node=ISARefNode("c") * ISARefNode("c"), + ).enumerate(ctx) + + # Multiple bindings via nesting + BindingNode( + name="c", + component=ExampleErrorCorrection.q(), + node=BindingNode( + name="f", + component=ExampleFactory.q(source=ISARefNode("c")), + node=ISARefNode("c") * ISARefNode("f"), + ), + ).enumerate(ctx) + """ + + name: str + component: ISAQuery + node: ISAQuery + + def enumerate(self, ctx: ISAContext) -> Generator[ISA, None, None]: + """ + Enumerate child nodes with the bound component in context. + + Args: + ctx (Context): The enumeration context. + + Yields: + ISA: An ISA instance from the child node. + """ + # Enumerate all ISAs from the component node + for isa in self.component.enumerate(ctx): + # Add binding to context and enumerate child node + new_ctx = ctx._with_binding(self.name, isa) + yield from self.node.enumerate(new_ctx) + + def populate(self, ctx: ISAContext) -> int: + """Populate the graph from both the component and the child node. + + Returns: + int: The starting node index of the component's additions. + """ + comp_start = self.component.populate(ctx) + self.node.populate(ctx) + return comp_start diff --git a/source/qdk_package/qdk/qre/_qre.py b/source/qdk_package/qdk/qre/_qre.py new file mode 100644 index 0000000000..2d1aaa7aa5 --- /dev/null +++ b/source/qdk_package/qdk/qre/_qre.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# flake8: noqa E402 +# pyright: reportAttributeAccessIssue=false + +from .._native import ( + _binom_ppf, + block_linear_function, + Block, + constant_function, + Constraint, + ConstraintBound, + _estimate_parallel, + _estimate_with_graph, + _EstimationCollection, + EstimationResult, + FactoryResult, + _FloatFunction, + generic_function, + instruction_name, + Instruction, + InstructionFrontier, + _IntFunction, + ISA, + ISARequirements, + _ProvenanceGraph, + linear_function, + LatticeSurgery, + PSSPC, + Trace, + property_name_to_key, + property_name, + _float_to_bits, + _float_from_bits, +) diff --git a/source/pip/qsharp/qre/_qre.pyi b/source/qdk_package/qdk/qre/_qre.pyi similarity index 100% rename from source/pip/qsharp/qre/_qre.pyi rename to source/qdk_package/qdk/qre/_qre.pyi diff --git a/source/pip/qsharp/qre/_results.py b/source/qdk_package/qdk/qre/_results.py similarity index 100% rename from source/pip/qsharp/qre/_results.py rename to source/qdk_package/qdk/qre/_results.py diff --git a/source/pip/qsharp/qre/_trace.py b/source/qdk_package/qdk/qre/_trace.py similarity index 100% rename from source/pip/qsharp/qre/_trace.py rename to source/qdk_package/qdk/qre/_trace.py diff --git a/source/qdk_package/qdk/qre/application.py b/source/qdk_package/qdk/qre/application.py deleted file mode 100644 index b7c33c4ef2..0000000000 --- a/source/qdk_package/qdk/qre/application.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -# flake8: noqa F403 -# pyright: ignore[reportWildcardImportFromLibrary] - -"""QRE application definitions. - -This module re-exports all public symbols from [qsharp.qre.application](:mod:`qsharp.qre.application`), -making them available under the ``qdk.qre.application`` namespace. It provides -classes for defining quantum applications to be passed to the resource estimator. - -Requires the ``qre`` extra: ``pip install qdk[qre]``. - -Example: - - from qdk.qre.application import QSharpApplication -""" - -try: - # Re-export the top-level qsharp.qre.application names. - from qsharp.qre.application import * -except Exception as ex: - raise ImportError( - "qdk.qre.application requires the qre extras. Install with 'pip install \"qdk[qre]\"'." - ) from ex diff --git a/source/qdk_package/qdk/qre/application/__init__.py b/source/qdk_package/qdk/qre/application/__init__.py new file mode 100644 index 0000000000..f6ee4c9f08 --- /dev/null +++ b/source/qdk_package/qdk/qre/application/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from ._cirq import CirqApplication +from ._qir import QIRApplication +from ._qsharp import QSharpApplication +from ._openqasm import OpenQASMApplication + +__all__ = [ + "CirqApplication", + "QIRApplication", + "QSharpApplication", + "OpenQASMApplication", +] diff --git a/source/pip/qsharp/qre/application/_cirq.py b/source/qdk_package/qdk/qre/application/_cirq.py similarity index 100% rename from source/pip/qsharp/qre/application/_cirq.py rename to source/qdk_package/qdk/qre/application/_cirq.py diff --git a/source/pip/qsharp/qre/application/_openqasm.py b/source/qdk_package/qdk/qre/application/_openqasm.py similarity index 96% rename from source/pip/qsharp/qre/application/_openqasm.py rename to source/qdk_package/qdk/qre/application/_openqasm.py index 53c525e54e..ed75112c53 100644 --- a/source/pip/qsharp/qre/application/_openqasm.py +++ b/source/qdk_package/qdk/qre/application/_openqasm.py @@ -46,7 +46,7 @@ def get_trace(self, parameters: None = None) -> Trace: Trace: The resource estimation trace. """ if isinstance(self.program, str): - from qsharp.openqasm import import_openqasm, ProgramType + from ...openqasm import import_openqasm, ProgramType name_found = False for _ in range(1_000): diff --git a/source/pip/qsharp/qre/application/_qir.py b/source/qdk_package/qdk/qre/application/_qir.py similarity index 100% rename from source/pip/qsharp/qre/application/_qir.py rename to source/qdk_package/qdk/qre/application/_qir.py diff --git a/source/pip/qsharp/qre/application/_qsharp.py b/source/qdk_package/qdk/qre/application/_qsharp.py similarity index 100% rename from source/pip/qsharp/qre/application/_qsharp.py rename to source/qdk_package/qdk/qre/application/_qsharp.py diff --git a/source/qdk_package/qdk/qre/instruction_ids.py b/source/qdk_package/qdk/qre/instruction_ids.py index 4b6f81c4b8..cec4a9c070 100644 --- a/source/qdk_package/qdk/qre/instruction_ids.py +++ b/source/qdk_package/qdk/qre/instruction_ids.py @@ -1,27 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# flake8: noqa F403 -# pyright: ignore[reportWildcardImportFromLibrary] +# pyright: reportAttributeAccessIssue=false -"""QRE instruction identifiers. -This module re-exports all public symbols from [qsharp.qre.instruction_ids](:mod:`qsharp.qre.instruction_ids`), -making them available under the ``qdk.qre.instruction_ids`` namespace. It provides -constants identifying the quantum instruction set operations used in resource -estimation traces. +from .._native import instruction_ids -Requires the ``qre`` extra: ``pip install qdk[qre]``. - -Example: - - from qdk.qre.instruction_ids import * -""" - -try: - # Re-export the top-level qsharp.qre.instruction_ids names. - from qsharp.qre.instruction_ids import * -except Exception as ex: - raise ImportError( - "qdk.qre.instruction_ids requires the qre extras. Install with 'pip install \"qdk[qre]\"'." - ) from ex +for name in instruction_ids.__all__: + globals()[name] = getattr(instruction_ids, name) diff --git a/source/pip/qsharp/qre/instruction_ids.pyi b/source/qdk_package/qdk/qre/instruction_ids.pyi similarity index 100% rename from source/pip/qsharp/qre/instruction_ids.pyi rename to source/qdk_package/qdk/qre/instruction_ids.pyi diff --git a/source/qdk_package/qdk/qre/interop.py b/source/qdk_package/qdk/qre/interop.py deleted file mode 100644 index 5d41c9fbb3..0000000000 --- a/source/qdk_package/qdk/qre/interop.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -# flake8: noqa F403 -# pyright: ignore[reportWildcardImportFromLibrary] - -"""QRE interoperability utilities. - -This module re-exports all public symbols from [qsharp.qre.interop](:mod:`qsharp.qre.interop`), -making them available under the ``qdk.qre.interop`` namespace. It provides -functions for generating resource estimation traces from Q#, Cirq, QIR, and -OpenQASM programs. - -Requires the ``qre`` extra: ``pip install qdk[qre]``. - -Example: - - from qdk.qre.interop import trace_from_qir -""" - -try: - # Re-export the top-level qsharp.qre.interop names. - from qsharp.qre.interop import * -except Exception as ex: - raise ImportError( - "qdk.qre.interop requires the qre extras. Install with 'pip install \"qdk[qre]\"'." - ) from ex diff --git a/source/qdk_package/qdk/qre/interop/__init__.py b/source/qdk_package/qdk/qre/interop/__init__.py new file mode 100644 index 0000000000..52917a3a42 --- /dev/null +++ b/source/qdk_package/qdk/qre/interop/__init__.py @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from ._cirq import ( + PeakUsageGreedyQubitManager, + PopBlock, + PushBlock, + QubitType, + ReadFromMemoryGate, + TypedQubit, + WriteToMemoryGate, + assert_qubits_type, + read_from_memory, + trace_from_cirq, + write_to_memory, +) +from ._qir import trace_from_qir +from ._qsharp import trace_from_entry_expr, trace_from_entry_expr_cached + +__all__ = [ + "trace_from_cirq", + "trace_from_entry_expr", + "trace_from_entry_expr_cached", + "trace_from_qir", + "PushBlock", + "PopBlock", + "QubitType", + "TypedQubit", + "PeakUsageGreedyQubitManager", + "ReadFromMemoryGate", + "WriteToMemoryGate", + "write_to_memory", + "read_from_memory", + "assert_qubits_type", +] diff --git a/source/qdk_package/qdk/qre/interop/_cirq.py b/source/qdk_package/qdk/qre/interop/_cirq.py new file mode 100644 index 0000000000..8665d8252a --- /dev/null +++ b/source/qdk_package/qdk/qre/interop/_cirq.py @@ -0,0 +1,822 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from __future__ import annotations + +import random +from dataclasses import dataclass +from enum import Enum +from math import pi +from typing import Iterable, Iterator, Sequence, cast + +import cirq +from cirq import ( + CCXPowGate, + CCZPowGate, + ClassicallyControlledOperation, + CXPowGate, + CZPowGate, + GateOperation, + HPowGate, + MeasurementGate, + PhaseGradientGate, + ResetChannel, + SwapPowGate, + XPowGate, + YPowGate, + ZPowGate, +) + +from qdk.qre import Block, Trace +from qdk.qre.instruction_ids import ( + CCX, + CCZ, + CX, + CZ, + MEAS_Z, + PAULI_X, + PAULI_Y, + PAULI_Z, + READ_FROM_MEMORY, + RX, + RY, + RZ, + S_DAG, + SQRT_SQRT_X, + SQRT_SQRT_X_DAG, + SQRT_SQRT_Y, + SQRT_SQRT_Y_DAG, + SQRT_X, + SQRT_X_DAG, + SQRT_Y, + SQRT_Y_DAG, + SWAP, + T_DAG, + WRITE_TO_MEMORY, + H, + S, + T, +) + +_TOLERANCE = 1e-8 + + +def _approx_eq(a: float, b: float) -> bool: + """Check whether two floats are approximately equal.""" + return abs(a - b) <= _TOLERANCE + + +def trace_from_cirq( + circuit: cirq.CIRCUIT_LIKE, + *, + classical_control_probability: float = 0.5, + rotation_threshold: float = 1e-6, + track_memory_qubits: bool = True, +) -> Trace: + """Convert a Cirq circuit into a resource estimation Trace. + + Iterates through all moments and operations in the circuit, converting + each gate into trace operations. Gates with a ``_to_trace`` method are + converted directly; others are recursively decomposed via Cirq's + ``_decompose_with_context_`` or ``_decompose_`` protocols. + + Args: + circuit: The Cirq circuit to convert. + classical_control_probability: Probability that a classically + controlled operation is included in the trace. Defaults to 0.5. + rotation_threshold: Rotation exponents with absolute value below + this threshold are treated as identity and omitted from the + trace. This applies to single-qubit rotations (RX, RY, RZ) as + well as to the rotation components of controlled-Z + decompositions. Defaults to 1e-6. + track_memory_qubits (bool): When True, memory qubits are tracked + separately from compute qubits. When False, all qubits are treated + as compute qubits. Also, if True, read-from-memory and + write-to-memory instructions are preserved in the trace, otherwise, + they are decompsed into SWAP and RESET instructions. Defaults to + True. + + Returns: + Trace: A Trace representing an execution profile of the circuit. + """ + + if isinstance(circuit, cirq.Circuit): + # circuit is already in the expected format, so we can process it directly. + pass + elif isinstance(circuit, cirq.Gate): + circuit = cirq.Circuit(circuit.on(*cirq.LineQid.for_gate(circuit))) + else: + # circuit is OP_TREE + circuit = cirq.Circuit(circuit) + + context = _CirqTraceBuilder( + circuit, classical_control_probability, rotation_threshold, track_memory_qubits + ) + + for moment in circuit: + for op in moment.operations: + context.handle_op(op) + + return context.trace + + +class _CirqTraceBuilder: + """Builds a resource estimation ``Trace`` from a Cirq circuit. + + This class walks the operations produced by ``trace_from_cirq`` and + translates each one into trace instructions. It maintains the state + needed during the conversion: + + * A ``Trace`` instance that accumulates the result. + * A stack of ``Block`` objects so that ``PushBlock`` / ``PopBlock`` + markers can create nested repeated sections. + * A qubit-id mapping (``_QidToTraceId``) that assigns each Cirq qubit + a sequential integer index. + * A Cirq ``DecompositionContext`` for gates that need recursive + decomposition. + + Args: + circuit: The Cirq circuit being converted. + classical_control_probability: Probability that a classically + controlled operation is included in the trace. + rotation_threshold: Rotation exponents with absolute value below + this threshold are treated as identity. + """ + + def __init__( + self, + circuit: cirq.Circuit, + classical_control_probability: float, + rotation_threshold: float, + track_memory_qubits: bool = True, + ): + self._circuit = circuit + self._trace = Trace(0) + self._classical_control_probability = classical_control_probability + self._rotation_threshold = rotation_threshold + self._track_memory_qubits = track_memory_qubits + self._blocks = [self._trace.root_block()] + self._q_to_id = _QidToTraceId(circuit.all_qubits()) + self._decomp_context = cirq.DecompositionContext( + qubit_manager=PeakUsageGreedyQubitManager( + "trace_from_cirq", size=0, maximize_reuse=True + ) + ) + + def push_block(self, repetitions: int): + """Open a new repeated block with the given number of repetitions.""" + block = self.block.add_block(repetitions) + self._blocks.append(block) + + def pop_block(self): + """Close the current repeated block, returning to the parent.""" + self._blocks.pop() + + @property + def trace(self) -> Trace: + """Determine compute and memory qubits from the circuit's qubits as well + as from the qubit manager before returning the trace.""" + + qm = cast(PeakUsageGreedyQubitManager, self._decomp_context.qubit_manager) + num_memory_qubits, num_compute_qubits = 0, 0 + + for q in self._circuit.all_qubits(): + if ( + self._track_memory_qubits + and isinstance(q, TypedQubit) + and q.qubit_type == QubitType.MEMORY + ): + num_memory_qubits += 1 + else: + # Untyped qubits are considered COMPUTE by default. + num_compute_qubits += 1 + + if self._track_memory_qubits: + num_memory_qubits += qm.memory_qubit_count() + else: + num_compute_qubits += qm.memory_qubit_count() + num_compute_qubits += qm.compute_qubit_count() + + self._trace.compute_qubits = num_compute_qubits + if self._track_memory_qubits and num_memory_qubits > 0: + self._trace.memory_qubits = num_memory_qubits + + return self._trace + + @property + def block(self) -> Block: + """The innermost open block in the trace.""" + return self._blocks[-1] + + @property + def q_to_id(self) -> _QidToTraceId: + """Mapping from Cirq ``Qid`` to integer trace qubit index.""" + return self._q_to_id + + @property + def classical_control_probability(self) -> float: + """Probability used to stochastically include classically controlled + operations.""" + return self._classical_control_probability + + @property + def rotation_threshold(self) -> float: + """Rotation exponents with absolute value below this threshold are + treated as identity.""" + return self._rotation_threshold + + @property + def decomp_context(self) -> cirq.DecompositionContext: + """Cirq decomposition context shared across all recursive + decompositions.""" + return self._decomp_context + + def handle_op( + self, + op: cirq.OP_TREE | TraceGate | PushBlock | PopBlock, + ) -> None: + """Recursively convert a single operation into trace instructions. + + Supported operation forms: + + - ``TraceGate``: A raw trace instruction, added directly to the + current block. + - ``PushBlock`` / ``PopBlock``: Control block nesting with + repetitions. + - ``GateOperation``: Dispatched via ``_to_trace`` if available on + the gate, otherwise decomposed via + ``_decompose_with_context_`` or ``_decompose_``. + - ``ClassicallyControlledOperation``: Included with the probability + given by ``classical_control_probability``. + - ``list`` / iterable: Each element is handled recursively. + - Any other ``cirq.Operation``: Decomposed via + ``_decompose_with_context_``. + + Args: + op: The operation to convert. + """ + if isinstance(op, TraceGate): + qs = [ + self.q_to_id[q] + for q in ([op.qubits] if isinstance(op.qubits, cirq.Qid) else op.qubits) + ] + + if op.params is None: + self.block.add_operation(op.id, qs) + else: + self.block.add_operation( + op.id, qs, op.params if isinstance(op.params, list) else [op.params] + ) + elif isinstance(op, PushBlock): + self.push_block(op.repetitions) + elif isinstance(op, PopBlock): + self.pop_block() + elif isinstance(op, cirq.Operation): + if isinstance(op, GateOperation): + gate = op.gate + + if hasattr(gate, "_to_trace"): + for sub_op in gate._to_trace(self, op): # type: ignore + self.handle_op(sub_op) + elif hasattr(gate, "_decompose_with_context_"): + for sub_op in gate._decompose_with_context_(op.qubits, self.decomp_context): # type: ignore + self.handle_op(sub_op) + elif hasattr(gate, "_decompose_"): + # decompose the gate and handle the resulting operations recursively + for sub_op in gate._decompose_(op.qubits): # type: ignore + self.handle_op(sub_op) + else: + for sub_op in op._decompose_with_context_(self.decomp_context): # type: ignore + self.handle_op(sub_op) + elif isinstance(op, ClassicallyControlledOperation): + if random.random() < self.classical_control_probability: + self.handle_op(op.without_classical_controls()) + elif isinstance(op, cirq.CircuitOperation): + if isinstance(op.repetitions, int): + self.push_block(op.repetitions) + for sub_op in op.circuit: # type: ignore + self.handle_op(sub_op) + self.pop_block() + else: + raise ValueError( + "Only integer repetitions are supported for CircuitOperation." + ) + else: + for sub_op in op._decompose_with_context_(self.decomp_context): # type: ignore + self.handle_op(sub_op) + else: + # op is Iterable[OP_TREE] + for sub_op in op: + self.handle_op(sub_op) + + +@dataclass(frozen=True, slots=True) +class PushBlock: + """Signals the start of a repeated block in the trace. + + Args: + repetitions: Number of times the block is repeated. + """ + + repetitions: int + + +@dataclass(frozen=True, slots=True) +class PopBlock: + """Signals the end of the current repeated block in the trace.""" + + ... + + +@dataclass(frozen=True, slots=True) +class TraceGate: + """A raw trace instruction emitted during Cirq circuit conversion. + + Attributes: + id (int): The instruction ID. + qubits (list[cirq.Qid] | cirq.Qid): The target qubits. + params (list[float] | float | None): Optional gate parameters. + """ + + id: int + qubits: list[cirq.Qid] | cirq.Qid + params: list[float] | float | None = None + + +class _QidToTraceId(dict): + """Mapping from Cirq qubits to integer trace qubit indices. + + Initialized with a set of known qubits. If an unknown qubit is looked + up, it is automatically assigned the next available index. + """ + + def __init__(self, init: Iterable[cirq.Qid]): + super().__init__({q: i for i, q in enumerate(init)}) + + def __getitem__(self, key: cirq.Qid) -> int: + """ + If the key is not present, add it to the mapping with the next available id. + """ + + if key not in self: + self[key] = len(self) + return super().__getitem__(key) + + +def h_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): + """Convert an HPowGate into trace instructions.""" + if _approx_eq(abs(self.exponent), 1): + yield TraceGate(H, [op.qubits[0]]) + else: + yield from op._decompose_with_context_(context.decomp_context) # type: ignore + + +def x_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): + """Convert an XPowGate into trace instructions.""" + q = [op.qubits[0]] + exp = self.exponent + if _approx_eq(exp, 1) or _approx_eq(exp, -1): + yield TraceGate(PAULI_X, q) + elif _approx_eq(exp, 0.5): + yield TraceGate(SQRT_X, q) + elif _approx_eq(exp, -0.5): + yield TraceGate(SQRT_X_DAG, q) + elif _approx_eq(exp, 0.25): + yield TraceGate(SQRT_SQRT_X, q) + elif _approx_eq(exp, -0.25): + yield TraceGate(SQRT_SQRT_X_DAG, q) + else: + if abs(exp) >= context.rotation_threshold: + yield TraceGate(RX, q, exp * pi) + + +def y_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): + """Convert a YPowGate into trace instructions.""" + q = [op.qubits[0]] + exp = self.exponent + if _approx_eq(exp, 1) or _approx_eq(exp, -1): + yield TraceGate(PAULI_Y, q) + elif _approx_eq(exp, 0.5): + yield TraceGate(SQRT_Y, q) + elif _approx_eq(exp, -0.5): + yield TraceGate(SQRT_Y_DAG, q) + elif _approx_eq(exp, 0.25): + yield TraceGate(SQRT_SQRT_Y, q) + elif _approx_eq(exp, -0.25): + yield TraceGate(SQRT_SQRT_Y_DAG, q) + else: + if abs(exp) >= context.rotation_threshold: + yield TraceGate(RY, q, exp * pi) + + +def z_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): + """Convert a ZPowGate into trace instructions.""" + q = [op.qubits[0]] + exp = self.exponent + if _approx_eq(exp, 1) or _approx_eq(exp, -1): + yield TraceGate(PAULI_Z, q) + elif _approx_eq(exp, 0.5): + yield TraceGate(S, q) + elif _approx_eq(exp, -0.5): + yield TraceGate(S_DAG, q) + elif _approx_eq(exp, 0.25): + yield TraceGate(T, q) + elif _approx_eq(exp, -0.25): + yield TraceGate(T_DAG, q) + else: + if abs(exp) >= context.rotation_threshold: + yield TraceGate(RZ, q, exp * pi) + + +def cx_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): + """Convert a CXPowGate into trace instructions.""" + if _approx_eq(abs(self.exponent), 1): + yield TraceGate(CX, [op.qubits[0], op.qubits[1]]) + else: + yield from op._decompose_with_context_(context.decomp_context) # type: ignore + + +def cz_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): + """Convert a CZPowGate into trace instructions.""" + exp = self.exponent + c, t = op.qubits[0], op.qubits[1] + if _approx_eq(abs(exp), 1): + yield TraceGate(CZ, [c, t]) + elif _approx_eq(exp, 0.5): + # controlled S gate + yield TraceGate(T, [c]) + yield TraceGate(T, [t]) + yield TraceGate(CZ, [c, t]) + yield TraceGate(T_DAG, [t]) + yield TraceGate(CZ, [c, t]) + elif _approx_eq(exp, -0.5): + # controlled S† gate + yield TraceGate(T_DAG, [c]) + yield TraceGate(T_DAG, [t]) + yield TraceGate(CZ, [c, t]) + yield TraceGate(T, [t]) + yield TraceGate(CZ, [c, t]) + else: + half_exp = exp / 2 + if abs(half_exp) >= context.rotation_threshold: + rads = half_exp * pi + yield TraceGate(RZ, [c], [rads]) + yield TraceGate(RZ, [t], [rads]) + yield TraceGate(CZ, [c, t]) + yield TraceGate(RZ, [t], [-rads]) + yield TraceGate(CZ, [c, t]) + + +def swap_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): + """Convert a SwapPowGate into trace instructions.""" + if _approx_eq(abs(self.exponent), 1): + yield TraceGate(SWAP, [op.qubits[0], op.qubits[1]]) + else: + yield from op._decompose_with_context_(context.decomp_context) # type: ignore + + +def ccx_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): + """Convert a CCXPowGate into trace instructions.""" + if _approx_eq(abs(self.exponent), 1): + yield TraceGate(CCX, [op.qubits[0], op.qubits[1], op.qubits[2]]) + else: + yield from op._decompose_with_context_(context.decomp_context) # type: ignore + + +def ccz_pow_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): + """Convert a CCZPowGate into trace instructions.""" + if _approx_eq(abs(self.exponent), 1): + yield TraceGate(CCZ, [op.qubits[0], op.qubits[1], op.qubits[2]]) + else: + yield from op._decompose_with_context_(context.decomp_context) # type: ignore + + +def measurement_gate_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): + """Convert a MeasurementGate into trace instructions.""" + for q in op.qubits: + yield TraceGate(MEAS_Z, [q]) + + +def reset_channel_to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation): + """Convert a ResetChannel into trace instructions (no-op).""" + yield from () + + +# Attach _to_trace methods to Cirq gate classes so that handle_op can +# convert them directly into trace instructions without decomposition. +HPowGate._to_trace = h_pow_gate_to_trace +XPowGate._to_trace = x_pow_gate_to_trace +YPowGate._to_trace = y_pow_gate_to_trace +ZPowGate._to_trace = z_pow_gate_to_trace +CXPowGate._to_trace = cx_pow_gate_to_trace +CZPowGate._to_trace = cz_pow_gate_to_trace +SwapPowGate._to_trace = swap_pow_gate_to_trace +CCXPowGate._to_trace = ccx_pow_gate_to_trace +CCZPowGate._to_trace = ccz_pow_gate_to_trace +MeasurementGate._to_trace = measurement_gate_to_trace +ResetChannel._to_trace = reset_channel_to_trace + +# Decomposition overrides + + +def phase_gradient_decompose(self, qubits): + """Override PhaseGradientGate._decompose_ to skip rotations with very small angles. + + The original implementation may lead to floating-point overflows for + large values of i. + """ + + for i, q in enumerate(qubits): + exp = self.exponent / 2**i + if abs(exp) < 1e-6: + break + yield cirq.Z(q) ** exp + + +PhaseGradientGate._decompose_ = phase_gradient_decompose + + +class QubitType(Enum): + """Qubit type. + + Each logical qubit can be either a compute or memory qubit. Compute qubits + can be used normally. + + Memory qubits have a restriction that gates cannot be applied to them. The + only allowed operations on memory qubits are reads/writes, where state is + moved from memory to compute gate or from compute to memory gate. + + We assume that when error correction is applied, memory qubits are encoded + with a more efficient error correction scheme requiring less resources, but + not allowing gate application (e.g. Yoked surface codes, + https://arxiv.org/abs/2312.04522). + """ + + COMPUTE = 1 + MEMORY = 2 + + +class TypedQubit(cirq.Qid): + """Qubit with type.""" + + def __init__( + self, + qubit: cirq.Qid, + qubit_type: QubitType, + ): + """Initializes typed qubit.""" + self._qubit = qubit + self.qubit_type = qubit_type + + def _comparison_key(self) -> object: + """Comparison key.""" + return self._qubit._comparison_key() + + @property + def dimension(self) -> int: + """Dimension.""" + return cast("int", self._qubit.dimension) + + def __repr__(self) -> str: + """String representation of the qubit.""" + return repr(self._qubit) + + +def _as_typed_qubit(q: cirq.Qid) -> TypedQubit: + """Converts qubit to TypedQubit.""" + assert isinstance(q, TypedQubit) + return q + + +def assert_qubits_type(qs: Sequence[cirq.Qid], qubit_type: QubitType) -> None: + """Asserts that qubits have specified type, but only if they are TypedQubits.""" + if len(qs) == 0 or not isinstance(qs[0], TypedQubit): + return + + for q in qs: + actual_type = _as_typed_qubit(q).qubit_type + assert ( + actual_type == qubit_type + ), f"{q} expected to be {qubit_type}, was {actual_type}." + + +class _TypedQubitManager(cirq.GreedyQubitManager): + """Qubit manager managing qubits of specified type. + + All allocated qubits will have specified type. + Tracks current and peak number of qubits. + """ + + def __init__( + self, prefix: str, qubit_type: QubitType, *, size: int, maximize_reuse: bool + ): + """Initialize the manager.""" + prefix = prefix + "_" + qubit_type.name[0] + super().__init__(prefix, size=size, maximize_reuse=maximize_reuse) + self.qubit_type = qubit_type + self.current_in_use = 0 + self.peak_in_use = 0 + + def _allocate_qid(self, name: str, dim: int) -> cirq.Qid: + """Allocates single qubit.""" + return TypedQubit(super()._allocate_qid(name, dim), self.qubit_type) + + def qalloc(self, n: int, dim: int) -> list[cirq.Qid]: + """Allocate ``n`` qubits and update the usage counters.""" + qs = super().qalloc(n, dim) + self.current_in_use += len(qs) + self.peak_in_use = max(self.peak_in_use, self.current_in_use) + return cast("list[cirq.Qid]", qs) + + def qfree(self, qubits: Iterable[cirq.Qid]) -> None: + """Free the given qubits and update the usage counters.""" + super().qfree(qubits) + self.current_in_use -= len(set(qubits)) + + +class PeakUsageGreedyQubitManager(cirq.QubitManager): + """A qubit manager tracking compute and memory qubits separately. + + It consists of two independent qubit managers for each qubit type. Each manager + uses greedy allocation strategy from ``cirq.GreedyQubitManager``. + + Qubits of one type, after freed, cannot be reused as qubits of different type. + Therefore, peak qubit count is equal to sum of peak qubit counts for each type. + """ + + def __init__(self, prefix: str, *, size: int, maximize_reuse: bool): + """Initialize the PeakUsageGreedyQubitManager. + + Args: + prefix: Naming prefix for allocated qubits. + size: Initial pool size passed through to ``cirq.GreedyQubitManager``. + Example: 0. + maximize_reuse: Flag to control qubit reuse strategy. If ``False``, this + mode uses a FIFO (First in First out) strategy s.t. next allocated qubit + is one which was freed the earliest. If ``True``, this mode uses a LIFO + (Last in First out) strategy s.t. the next allocated qubit is one which + was freed the latest. + + """ + self.typed_managers = { + qubit_type: _TypedQubitManager( + prefix, qubit_type, size=size, maximize_reuse=maximize_reuse + ) + for qubit_type in QubitType + } + + def qalloc( + self, n: int, dim: int, qubit_type: QubitType = QubitType.COMPUTE + ) -> list[cirq.Qid]: + """Allocate ``n`` qubits and update the usage counters. + + Args: + n: Number of qubits to allocate. + dim: Dimension of each qubit. Example: 2 for qubits. + qubit_type: Type of qubits (COMPUTE or MEMORY). + + Returns: + List of allocated qubits. + + """ + return self.typed_managers[qubit_type].qalloc(n, dim) + + def qborrow(self, n: int, dim: int = 2) -> list[cirq.Qid]: + """Borrow qubits (not supported).""" + raise NotImplementedError("qborrow is not supported.") + + def qfree(self, qubits: Iterable[cirq.Qid]) -> None: + """Free the given qubits.""" + qubits_by_type: dict[QubitType, list[cirq.Qid]] = {t: [] for t in QubitType} + for q in qubits: + qubits_by_type[_as_typed_qubit(q).qubit_type].append(q) + for qubit_type, qs in qubits_by_type.items(): + if len(qs) > 0: + self.typed_managers[qubit_type].qfree(qs) + + def current_in_use(self) -> int: + """Number of qubits currently in use.""" + return sum(qm.current_in_use for qm in self.typed_managers.values()) + + def qubit_count(self) -> int: + """Returns the peak number of qubits of all types. + + It is equal to sum of peak counts for each type, because qubits of one type + cannot be reused as qubits of a different type. + """ + return self.compute_qubit_count() + self.memory_qubit_count() + + def compute_qubit_count(self) -> int: + """Returns the peak number of simultaneously in-use COMPUTE qubits.""" + return self.typed_managers[QubitType.COMPUTE].peak_in_use + + def memory_qubit_count(self) -> int: + """Returns the peak number of simultaneously in-use MEMORY qubits.""" + return self.typed_managers[QubitType.MEMORY].peak_in_use + + +class ReadFromMemoryGate(cirq.Gate): + """Moves qubit states from MEMORY register to COMPUTE register. + + Assumes COMPUTE qubits are prepared in 0 state. Leaves MEMORY qubits in 0 state. + """ + + def __init__(self, n: int): + """Initializes ReadFromMemoryGate.""" + self.n = n + + def _num_qubits_(self) -> int: + """Number of qubits passed in to this gate.""" + return 2 * self.n + + def _decompose_(self, qubits: Sequence[cirq.Qid]) -> Iterator[cirq.Operation]: + """Decomposes this gate into equivalent SWAP gates.""" + comp_qs, mem_qs = self._get_qubits(qubits) + for i in range(self.n): + yield cirq.reset(comp_qs[i]) + yield cirq.SWAP(mem_qs[i], comp_qs[i]) + + def _to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation, **_kwargs): + """Convert this gate into trace instructions.""" + if context._track_memory_qubits: + comp_qs, mem_qs = self._get_qubits(op.qubits) + for i in range(self.n): + yield TraceGate(READ_FROM_MEMORY, [mem_qs[i], comp_qs[i]]) + else: + yield from self._decompose_(op.qubits) + + def _get_qubits( + self, qubits: Sequence[cirq.Qid] + ) -> tuple[Sequence[cirq.Qid], Sequence[cirq.Qid]]: + """Get qubits for this gate partitioned into compute and memory + qubits.""" + assert len(qubits) == 2 * self.n + mem_qs = qubits[0 : self.n] + comp_qs = qubits[self.n : 2 * self.n] + assert_qubits_type(mem_qs, QubitType.MEMORY) + assert_qubits_type(comp_qs, QubitType.COMPUTE) + return comp_qs, mem_qs + + +class WriteToMemoryGate(cirq.Gate): + """Moves qubit states from COMPUTE register to MEMORY register. + + Assumes MEMORY qubits are prepared in 0 state. Leaves COMPUTE qubits in 0 state. + """ + + def __init__(self, n: int): + """Initializes WriteToMemoryGate.""" + self.n = n + + def _num_qubits_(self) -> int: + """Number of qubits passed in to this gate.""" + return 2 * self.n + + def _decompose_(self, qubits: Sequence[cirq.Qid]) -> Iterator[cirq.Operation]: + """Decomposes this gate into equivalent SWAP gates.""" + comp_qs, mem_qs = self._get_qubits(qubits) + for i in range(self.n): + yield cirq.reset(mem_qs[i]) + yield cirq.SWAP(mem_qs[i], comp_qs[i]) + + def _to_trace(self, context: _CirqTraceBuilder, op: cirq.Operation, **_kwargs): + """Convert this gate into trace instructions.""" + if context._track_memory_qubits: + comp_qs, mem_qs = self._get_qubits(op.qubits) + for i in range(self.n): + yield TraceGate(WRITE_TO_MEMORY, [comp_qs[i], mem_qs[i]]) + else: + yield from self._decompose_(op.qubits) + + def _get_qubits( + self, qubits: Sequence[cirq.Qid] + ) -> tuple[Sequence[cirq.Qid], Sequence[cirq.Qid]]: + assert len(qubits) == 2 * self.n + mem_qs = qubits[0 : self.n] + comp_qs = qubits[self.n : 2 * self.n] + assert_qubits_type(mem_qs, QubitType.MEMORY) + assert_qubits_type(comp_qs, QubitType.COMPUTE) + + return comp_qs, mem_qs + + +def write_to_memory( + memory_qubits: Sequence[cirq.Qid], compute_qubits: Sequence[cirq.Qid] +) -> cirq.Operation: + """Operation to write qubits to memory.""" + assert_qubits_type(memory_qubits, QubitType.MEMORY) + assert_qubits_type(compute_qubits, QubitType.COMPUTE) + n = len(memory_qubits) + assert n == len(compute_qubits) + return WriteToMemoryGate(n).on(*memory_qubits, *compute_qubits) + + +def read_from_memory( + memory_qubits: Sequence[cirq.Qid], compute_qubits: Sequence[cirq.Qid] +) -> cirq.Operation: + """Operation to read qubits from memory.""" + assert_qubits_type(memory_qubits, QubitType.MEMORY) + assert_qubits_type(compute_qubits, QubitType.COMPUTE) + n = len(memory_qubits) + assert n == len(compute_qubits) + return ReadFromMemoryGate(n).on(*memory_qubits, *compute_qubits) diff --git a/source/pip/qsharp/qre/interop/_qir.py b/source/qdk_package/qdk/qre/interop/_qir.py similarity index 98% rename from source/pip/qsharp/qre/interop/_qir.py rename to source/qdk_package/qdk/qre/interop/_qir.py index ebfb9559d1..dd6d50eff4 100644 --- a/source/pip/qsharp/qre/interop/_qir.py +++ b/source/qdk_package/qdk/qre/interop/_qir.py @@ -6,7 +6,7 @@ import pyqir from ..._native import QirInstructionId -from ..._simulation import AggregateGatesPass +from ...simulation._simulation import AggregateGatesPass from .. import instruction_ids as ids from .._qre import Trace diff --git a/source/pip/qsharp/qre/interop/_qsharp.py b/source/qdk_package/qdk/qre/interop/_qsharp.py similarity index 99% rename from source/pip/qsharp/qre/interop/_qsharp.py rename to source/qdk_package/qdk/qre/interop/_qsharp.py index e8a8e8fe12..d760dcf66f 100644 --- a/source/pip/qsharp/qre/interop/_qsharp.py +++ b/source/qdk_package/qdk/qre/interop/_qsharp.py @@ -7,7 +7,7 @@ import time from typing import Callable, Optional -from ..._qsharp import logical_counts +from ..._interpreter import logical_counts from ...estimator import LogicalCounts from .._qre import Trace from ..instruction_ids import CCX, MEAS_Z, RZ, T, READ_FROM_MEMORY, WRITE_TO_MEMORY diff --git a/source/qdk_package/qdk/qre/models.py b/source/qdk_package/qdk/qre/models.py deleted file mode 100644 index 2477b79f1a..0000000000 --- a/source/qdk_package/qdk/qre/models.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -# flake8: noqa F403 -# pyright: ignore[reportWildcardImportFromLibrary] - -"""QRE hardware and QEC models. - -This module re-exports all public symbols from [qsharp.qre.models](:mod:`qsharp.qre.models`), -making them available under the ``qdk.qre.models`` namespace. It provides -classes representing hardware architectures, qubit models, and quantum error -correction schemes used in resource estimation. - -Requires the ``qre`` extra: ``pip install qdk[qre]``. - -Example: - - from qdk.qre.models import SurfaceCode -""" - -try: - # Re-export the top-level qsharp.qre.models names. - from qsharp.qre.models import * -except Exception as ex: - raise ImportError( - "qdk.qre.models requires the qre extras. Install with 'pip install \"qdk[qre]\"'." - ) from ex diff --git a/source/qdk_package/qdk/qre/models/__init__.py b/source/qdk_package/qdk/qre/models/__init__.py new file mode 100644 index 0000000000..3da76797ac --- /dev/null +++ b/source/qdk_package/qdk/qre/models/__init__.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from .factories import Litinski19Factory, MagicUpToClifford, RoundBasedFactory +from .qec import ( + SurfaceCode, + ThreeAux, + OneDimensionalYokedSurfaceCode, + TwoDimensionalYokedSurfaceCode, +) +from .qubits import GateBased, Majorana + +__all__ = [ + "GateBased", + "Litinski19Factory", + "Majorana", + "MagicUpToClifford", + "RoundBasedFactory", + "SurfaceCode", + "ThreeAux", + "OneDimensionalYokedSurfaceCode", + "TwoDimensionalYokedSurfaceCode", +] diff --git a/source/pip/qsharp/qre/models/factories/__init__.py b/source/qdk_package/qdk/qre/models/factories/__init__.py similarity index 100% rename from source/pip/qsharp/qre/models/factories/__init__.py rename to source/qdk_package/qdk/qre/models/factories/__init__.py diff --git a/source/pip/qsharp/qre/models/factories/_litinski.py b/source/qdk_package/qdk/qre/models/factories/_litinski.py similarity index 100% rename from source/pip/qsharp/qre/models/factories/_litinski.py rename to source/qdk_package/qdk/qre/models/factories/_litinski.py diff --git a/source/pip/qsharp/qre/models/factories/_round_based.py b/source/qdk_package/qdk/qre/models/factories/_round_based.py similarity index 100% rename from source/pip/qsharp/qre/models/factories/_round_based.py rename to source/qdk_package/qdk/qre/models/factories/_round_based.py diff --git a/source/pip/qsharp/qre/models/factories/_utils.py b/source/qdk_package/qdk/qre/models/factories/_utils.py similarity index 100% rename from source/pip/qsharp/qre/models/factories/_utils.py rename to source/qdk_package/qdk/qre/models/factories/_utils.py diff --git a/source/pip/qsharp/qre/models/qec/__init__.py b/source/qdk_package/qdk/qre/models/qec/__init__.py similarity index 100% rename from source/pip/qsharp/qre/models/qec/__init__.py rename to source/qdk_package/qdk/qre/models/qec/__init__.py diff --git a/source/pip/qsharp/qre/models/qec/_surface_code.py b/source/qdk_package/qdk/qre/models/qec/_surface_code.py similarity index 100% rename from source/pip/qsharp/qre/models/qec/_surface_code.py rename to source/qdk_package/qdk/qre/models/qec/_surface_code.py diff --git a/source/pip/qsharp/qre/models/qec/_three_aux.py b/source/qdk_package/qdk/qre/models/qec/_three_aux.py similarity index 100% rename from source/pip/qsharp/qre/models/qec/_three_aux.py rename to source/qdk_package/qdk/qre/models/qec/_three_aux.py diff --git a/source/pip/qsharp/qre/models/qec/_yoked.py b/source/qdk_package/qdk/qre/models/qec/_yoked.py similarity index 100% rename from source/pip/qsharp/qre/models/qec/_yoked.py rename to source/qdk_package/qdk/qre/models/qec/_yoked.py diff --git a/source/pip/qsharp/qre/models/qubits/__init__.py b/source/qdk_package/qdk/qre/models/qubits/__init__.py similarity index 100% rename from source/pip/qsharp/qre/models/qubits/__init__.py rename to source/qdk_package/qdk/qre/models/qubits/__init__.py diff --git a/source/pip/qsharp/qre/models/qubits/_gate_based.py b/source/qdk_package/qdk/qre/models/qubits/_gate_based.py similarity index 100% rename from source/pip/qsharp/qre/models/qubits/_gate_based.py rename to source/qdk_package/qdk/qre/models/qubits/_gate_based.py diff --git a/source/pip/qsharp/qre/models/qubits/_msft.py b/source/qdk_package/qdk/qre/models/qubits/_msft.py similarity index 100% rename from source/pip/qsharp/qre/models/qubits/_msft.py rename to source/qdk_package/qdk/qre/models/qubits/_msft.py diff --git a/source/qdk_package/qdk/qre/property_keys.py b/source/qdk_package/qdk/qre/property_keys.py index 3288809e9b..a4ac1bdbef 100644 --- a/source/qdk_package/qdk/qre/property_keys.py +++ b/source/qdk_package/qdk/qre/property_keys.py @@ -1,30 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# flake8: noqa F403 -# pyright: ignore[reportWildcardImportFromLibrary] +# pyright: reportAttributeAccessIssue=false -"""QRE property key constants. -This module re-exports all public symbols from [qsharp.qre.property_keys](:mod:`qsharp.qre.property_keys`), -making them available under the ``qdk.qre.property_keys`` namespace. It also -provides helpers for defining custom property keys that don't conflict with -built-in ones. +from .._native import property_keys -Requires the ``qre`` extra: ``pip install qdk[qre]``. - -Example: - - from qdk.qre.property_keys import * -""" - -try: - # Re-export the top-level qsharp.qre.property_keys names. - from qsharp.qre.property_keys import * -except Exception as ex: - raise ImportError( - "qdk.qre.property_keys requires the qre extras. Install with 'pip install \"qdk[qre]\"'." - ) from ex +for name in property_keys.__all__: + globals()[name] = getattr(property_keys, name) # Some starting index for custom properties, to avoid conflicts with the # built-in ones. We do not expect to have more than 1 million built-in diff --git a/source/pip/qsharp/qre/property_keys.pyi b/source/qdk_package/qdk/qre/property_keys.pyi similarity index 100% rename from source/pip/qsharp/qre/property_keys.pyi rename to source/qdk_package/qdk/qre/property_keys.pyi diff --git a/source/qdk_package/qdk/qsharp.py b/source/qdk_package/qdk/qsharp.py index 5953233775..edf7953c76 100644 --- a/source/qdk_package/qdk/qsharp.py +++ b/source/qdk_package/qdk/qsharp.py @@ -12,10 +12,10 @@ - :func:`~qsharp.init`, :func:`~qsharp.eval`, :func:`~qsharp.run` — initialize and execute Q# code. - :class:`~qsharp.StateDump`, :class:`~qsharp.TargetProfile` — state inspection and compilation target. - :class:`~qsharp.PauliNoise`, :class:`~qsharp.DepolarizingNoise`, :class:`~qsharp.BitFlipNoise`, :class:`~qsharp.PhaseFlipNoise` — noise models. -- :func:`~qsharp.utils.dump_operation` — compute the unitary matrix of a Q# operation. +- :func:`~qdk.qsharp.dump_operation` — compute the unitary matrix of a Q# operation. For full API documentation see [qsharp](:mod:`qsharp`). """ -from qsharp import * # pyright: ignore[reportWildcardImportFromLibrary] -from qsharp.utils import dump_operation # pyright: ignore[reportUnusedImport] +from ._types import * # pyright: ignore[reportWildcardImportFromLibrary] +from ._interpreter import * # pyright: ignore[reportWildcardImportFromLibrary] diff --git a/source/qdk_package/qdk/simulation.py b/source/qdk_package/qdk/simulation/__init__.py similarity index 51% rename from source/qdk_package/qdk/simulation.py rename to source/qdk_package/qdk/simulation/__init__.py index 7c2a92f1a1..8e5701332c 100644 --- a/source/qdk_package/qdk/simulation.py +++ b/source/qdk_package/qdk/simulation/__init__.py @@ -5,29 +5,30 @@ This module exposes the core building blocks for noise-aware quantum simulation: -- :class:`~qsharp._device._atom.NeutralAtomDevice` — models a +- :class:`~qdk.simulation.NeutralAtomDevice` — models a neutral atom quantum device with configurable zone layouts, qubit registers, and movement constraints. Used to compile and simulate circuits on a realistic hardware topology. -- :class:`~qsharp._simulation.NoiseConfig` — configures per-gate Pauli noise +- :class:`~qdk.simulation.NoiseConfig` — configures per-gate Pauli noise (including qubit loss) for use with the Q# simulator. Assign noise tables to individual gate intrinsics to model depolarizing, bit-flip, phase-flip, or correlated noise channels. -- :func:`~qsharp._simulation.run_qir` — simulates QIR as given in one of +- :func:`~qdk.simulation.run_qir` — simulates QIR as given in one of three backend simulators: clifford, gpu or cpu. -- :class:`~qsharp.noisy_simulator.DensityMatrixSimulator` — an experimental simulator that uses +- :class:`~qdk.simulation.DensityMatrixSimulator` — an experimental simulator that uses a density-matrix to track its state. -- :class:`~qsharp.noisy_simulator.StateVectorSimulator` — an experimental simulator that uses +- :class:`~qdk.simulation.StateVectorSimulator` — an experimental simulator that uses a state-vector to track its state. """ -from qsharp._device._atom import NeutralAtomDevice -from qsharp._simulation import NoiseConfig, run_qir -from qsharp.noisy_simulator import ( +from .._device._atom import NeutralAtomDevice +from ._simulation import NoiseConfig, run_qir +from ._noisy_simulator import ( + NoisySimulatorError, DensityMatrixSimulator, StateVectorSimulator, DensityMatrix, @@ -35,3 +36,16 @@ Operation, Instrument, ) + +__all__ = [ + "NeutralAtomDevice", + "NoiseConfig", + "run_qir", + "NoisySimulatorError", + "Operation", + "Instrument", + "DensityMatrixSimulator", + "StateVectorSimulator", + "DensityMatrix", + "StateVector", +] diff --git a/source/pip/qsharp/noisy_simulator/_noisy_simulator.py b/source/qdk_package/qdk/simulation/_noisy_simulator.py similarity index 100% rename from source/pip/qsharp/noisy_simulator/_noisy_simulator.py rename to source/qdk_package/qdk/simulation/_noisy_simulator.py diff --git a/source/pip/qsharp/noisy_simulator/_noisy_simulator.pyi b/source/qdk_package/qdk/simulation/_noisy_simulator.pyi similarity index 100% rename from source/pip/qsharp/noisy_simulator/_noisy_simulator.pyi rename to source/qdk_package/qdk/simulation/_noisy_simulator.pyi diff --git a/source/qdk_package/qdk/simulation/_simulation.py b/source/qdk_package/qdk/simulation/_simulation.py new file mode 100644 index 0000000000..f8ae3f53f6 --- /dev/null +++ b/source/qdk_package/qdk/simulation/_simulation.py @@ -0,0 +1,742 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from pathlib import Path +import random +from typing import Callable, Literal, List, Optional, Tuple, TypeAlias, Union +import pyqir +from .._native import ( + QirInstructionId, + QirInstruction, + run_clifford, + run_clifford_adaptive, + run_parallel_shots, + run_adaptive_parallel_shots, + run_cpu_adaptive, + run_cpu_full_state, + NoiseConfig, + GpuContext, + try_create_gpu_adapter, + Result, +) +from pyqir import ( + Function, + FunctionType, + PointerType, + Type, + Linkage, +) +from .._types import QirInputData +from typing import TYPE_CHECKING +from .._adaptive_pass import ( + AdaptiveProfilePass, + AdaptiveProgram, + Bytecode, + OP_RECORD_OUTPUT, +) + +if TYPE_CHECKING: # This is in the pyi file only + from .._native import GpuShotResults + + +class AggregateGatesPass(pyqir.QirModuleVisitor): + def __init__(self): + super().__init__() + self.gates: List[QirInstruction | Tuple] = [] + self.required_num_qubits = None + self.required_num_results = None + + def _get_value_as_string(self, value: pyqir.Value) -> str: + value = pyqir.extract_byte_string(value) + if value is None: + return "" + value = value.decode("utf-8") + return value + + def run(self, mod: pyqir.Module) -> Tuple[List[QirInstruction | Tuple], int, int]: + errors = mod.verify() + if errors is not None: + raise ValueError(f"Module verification failed: {errors}") + + # verify that the module is base profile + func = next(filter(pyqir.is_entry_point, mod.functions)) + self.required_num_qubits = pyqir.required_num_qubits(func) + self.required_num_results = pyqir.required_num_results(func) + + super().run(mod) + return (self.gates, self.required_num_qubits, self.required_num_results) + + def _on_block(self, block): + if ( + block.terminator + and block.terminator.opcode == pyqir.Opcode.BR + and len(block.terminator.operands) > 1 + ): + raise ValueError( + "simulation of programs with branching control flow is not supported" + ) + super()._on_block(block) + + def _on_call_instr(self, call: pyqir.Call) -> None: + callee_name = call.callee.name + if callee_name == "__quantum__qis__ccx__body": + self.gates.append( + ( + QirInstructionId.CCX, + pyqir.ptr_id(call.args[0]), + pyqir.ptr_id(call.args[1]), + pyqir.ptr_id(call.args[2]), + ) + ) + elif callee_name == "__quantum__qis__cx__body": + self.gates.append( + ( + QirInstructionId.CX, + pyqir.ptr_id(call.args[0]), + pyqir.ptr_id(call.args[1]), + ) + ) + elif callee_name == "__quantum__qis__cy__body": + self.gates.append( + ( + QirInstructionId.CY, + pyqir.ptr_id(call.args[0]), + pyqir.ptr_id(call.args[1]), + ) + ) + elif callee_name == "__quantum__qis__cz__body": + self.gates.append( + ( + QirInstructionId.CZ, + pyqir.ptr_id(call.args[0]), + pyqir.ptr_id(call.args[1]), + ) + ) + elif callee_name == "__quantum__qis__swap__body": + self.gates.append( + ( + QirInstructionId.SWAP, + pyqir.ptr_id(call.args[0]), + pyqir.ptr_id(call.args[1]), + ) + ) + elif callee_name == "__quantum__qis__rx__body": + self.gates.append( + ( + QirInstructionId.RX, + call.args[0].value, + pyqir.ptr_id(call.args[1]), + ) + ) + elif callee_name == "__quantum__qis__rxx__body": + self.gates.append( + ( + QirInstructionId.RXX, + call.args[0].value, + pyqir.ptr_id(call.args[1]), + pyqir.ptr_id(call.args[2]), + ) + ) + elif callee_name == "__quantum__qis__ry__body": + self.gates.append( + ( + QirInstructionId.RY, + call.args[0].value, + pyqir.ptr_id(call.args[1]), + ) + ) + elif callee_name == "__quantum__qis__ryy__body": + self.gates.append( + ( + QirInstructionId.RYY, + call.args[0].value, + pyqir.ptr_id(call.args[1]), + pyqir.ptr_id(call.args[2]), + ) + ) + elif callee_name == "__quantum__qis__rz__body": + self.gates.append( + ( + QirInstructionId.RZ, + call.args[0].value, + pyqir.ptr_id(call.args[1]), + ) + ) + elif callee_name == "__quantum__qis__rzz__body": + self.gates.append( + ( + QirInstructionId.RZZ, + call.args[0].value, + pyqir.ptr_id(call.args[1]), + pyqir.ptr_id(call.args[2]), + ) + ) + elif callee_name == "__quantum__qis__h__body": + self.gates.append((QirInstructionId.H, pyqir.ptr_id(call.args[0]))) + elif callee_name == "__quantum__qis__s__body": + self.gates.append((QirInstructionId.S, pyqir.ptr_id(call.args[0]))) + elif callee_name == "__quantum__qis__s__adj": + self.gates.append((QirInstructionId.SAdj, pyqir.ptr_id(call.args[0]))) + elif callee_name == "__quantum__qis__sx__body": + self.gates.append((QirInstructionId.SX, pyqir.ptr_id(call.args[0]))) + elif callee_name == "__quantum__qis__t__body": + self.gates.append((QirInstructionId.T, pyqir.ptr_id(call.args[0]))) + elif callee_name == "__quantum__qis__t__adj": + self.gates.append((QirInstructionId.TAdj, pyqir.ptr_id(call.args[0]))) + elif callee_name == "__quantum__qis__x__body": + self.gates.append((QirInstructionId.X, pyqir.ptr_id(call.args[0]))) + elif callee_name == "__quantum__qis__y__body": + self.gates.append((QirInstructionId.Y, pyqir.ptr_id(call.args[0]))) + elif callee_name == "__quantum__qis__z__body": + self.gates.append((QirInstructionId.Z, pyqir.ptr_id(call.args[0]))) + elif callee_name == "__quantum__qis__m__body": + self.gates.append( + ( + QirInstructionId.M, + pyqir.ptr_id(call.args[0]), + pyqir.ptr_id(call.args[1]), + ) + ) + elif callee_name == "__quantum__qis__mz__body": + self.gates.append( + ( + QirInstructionId.MZ, + pyqir.ptr_id(call.args[0]), + pyqir.ptr_id(call.args[1]), + ) + ) + elif callee_name == "__quantum__qis__mresetz__body": + self.gates.append( + ( + QirInstructionId.MResetZ, + pyqir.ptr_id(call.args[0]), + pyqir.ptr_id(call.args[1]), + ) + ) + elif callee_name == "__quantum__qis__reset__body": + self.gates.append((QirInstructionId.RESET, pyqir.ptr_id(call.args[0]))) + elif callee_name == "__quantum__qis__move__body": + self.gates.append( + ( + QirInstructionId.Move, + pyqir.ptr_id(call.args[0]), + ) + ) + elif callee_name == "__quantum__rt__result_record_output": + tag = self._get_value_as_string(call.args[1]) + self.gates.append( + ( + QirInstructionId.ResultRecordOutput, + str(pyqir.ptr_id(call.args[0])), + tag, + ) + ) + elif callee_name == "__quantum__rt__tuple_record_output": + tag = self._get_value_as_string(call.args[1]) + self.gates.append( + (QirInstructionId.TupleRecordOutput, str(call.args[0].value), tag) + ) + elif callee_name == "__quantum__rt__array_record_output": + tag = self._get_value_as_string(call.args[1]) + self.gates.append( + (QirInstructionId.ArrayRecordOutput, str(call.args[0].value), tag) + ) + elif ( + callee_name == "__quantum__rt__initialize" + or callee_name == "__quantum__rt__begin_parallel" + or callee_name == "__quantum__rt__end_parallel" + or callee_name == "__quantum__qis__barrier__body" + # We only hit this during noiseless simulations + or "qdk_noise" in call.callee.attributes.func + ): + pass + else: + raise ValueError(f"Unsupported call instruction: {callee_name}") + + +class CorrelatedNoisePass(AggregateGatesPass): + """ + This pass replaces the QIR intrinsics that are in the provided NoiseConfig + by correlated noise instructions that the simulator understands. + """ + + def __init__(self, noise_config: NoiseConfig): + super().__init__() + self.noise_intrinsics_table = noise_config.intrinsics + + def _on_call_instr(self, call: pyqir.Call) -> None: + callee_name = call.callee.name + if callee_name in self.noise_intrinsics_table: + self.gates.append( + ( + QirInstructionId.CorrelatedNoise, + self.noise_intrinsics_table.get_intrinsic_id(callee_name), + [pyqir.ptr_id(arg) for arg in call.args], + ) + ) + elif "qdk_noise" in call.callee.attributes.func: + # If we are running a noisy simulation, we treat + # missing noise intrinsics as an error. + raise ValueError(f"Missing noise intrinsic: {callee_name}") + else: + super()._on_call_instr(call) + + +class GpuCorrelatedNoisePass(AggregateGatesPass): + """ + A special case of the CorrelatedNoisePass that uses data loaded + directly from rust instead of a NoiseConfig object to detect the + correlated noise intrinsics. + """ + + def __init__(self, noise_table: List[Tuple[int, str, int]]): + super().__init__() + self.noise_table = dict() + for table_id, name, _count in noise_table: + self.noise_table[name] = table_id + + def _on_call_instr(self, call: pyqir.Call) -> None: + callee_name = call.callee.name + if callee_name in self.noise_table: + self.gates.append( + ( + QirInstructionId.CorrelatedNoise, + int(self.noise_table[callee_name]), # Noise table ID + [pyqir.ptr_id(qubit) for qubit in call.args], # qubit args + ) + ) + elif "qdk_noise" in call.callee.attributes.func: + # If we are running a noisy simulation, we treat + # missing noise intrinsics as an error. + raise ValueError(f"Missing noise intrinsic: {callee_name}") + else: + super()._on_call_instr(call) + + +class OutputRecordingPass(pyqir.QirModuleVisitor): + _output_str = "" + _closers = [] + _counters = [] + + def process_output(self, bitstring: str): + return eval( + self._output_str, + { + "o": [ + Result.Zero if x == "0" else Result.One if x == "1" else Result.Loss + for x in bitstring + ] + }, + ) + + def _on_function(self, function): + if pyqir.is_entry_point(function): + super()._on_function(function) + while len(self._closers) > 0: + self._output_str += self._closers.pop() + self._counters.pop() + + def _on_rt_result_record_output(self, call, result, target): + self._output_str += f"o[{pyqir.ptr_id(result)}]" + while len(self._counters) > 0: + self._output_str += "," + self._counters[-1] -= 1 + if self._counters[-1] == 0: + self._output_str += self._closers[-1] + self._closers.pop() + self._counters.pop() + else: + break + + def _on_rt_array_record_output(self, call, value, target): + self._output_str += "[" + self._closers.append("]") + # if len(self._counters) > 0: + # self._counters[-1] -= 1 + self._counters.append(value.value) + + def _on_rt_tuple_record_output(self, call, value, target): + self._output_str += "(" + self._closers.append(")") + # if len(self._counters) > 0: + # self._counters[-1] -= 1 + self._counters.append(value.value) + + +class DecomposeCcxPass(pyqir.QirModuleVisitor): + + h_func: Function + t_func: Function + tadj_func: Function + cz_func: Function + + def __init__(self): + super().__init__() + + def _on_module(self, module): + void = Type.void(module.context) + qubit_ty = PointerType(Type.void(module.context)) + + # Find or create all the needed functions. + for func in module.functions: + match func.name: + case "__quantum__qis__h__body": + self.h_func = func + case "__quantum__qis__t__body": + self.t_func = func + case "__quantum__qis__t__adj": + self.tadj_func = func + case "__quantum__qis__cz__body": + self.cz_func = func + if not hasattr(self, "h_func"): + self.h_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__h__body", + module, + ) + if not hasattr(self, "t_func"): + self.t_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__t__body", + module, + ) + if not hasattr(self, "tadj_func"): + self.tadj_func = Function( + FunctionType(void, [qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__t__adj", + module, + ) + if not hasattr(self, "cz_func"): + self.cz_func = Function( + FunctionType(void, [qubit_ty, qubit_ty]), + Linkage.EXTERNAL, + "__quantum__qis__cz__body", + module, + ) + super()._on_module(module) + + def _on_qis_ccx(self, call, ctrl1, ctrl2, target): + self.builder.insert_before(call) + self.builder.call(self.h_func, [target]) + self.builder.call(self.tadj_func, [ctrl1]) + self.builder.call(self.tadj_func, [ctrl2]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.cz_func, [target, ctrl1]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.t_func, [ctrl1]) + self.builder.call(self.h_func, [target]) + self.builder.call(self.cz_func, [ctrl2, target]) + self.builder.call(self.h_func, [target]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.cz_func, [ctrl2, ctrl1]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.t_func, [target]) + self.builder.call(self.tadj_func, [ctrl1]) + self.builder.call(self.h_func, [target]) + self.builder.call(self.cz_func, [ctrl2, target]) + self.builder.call(self.h_func, [target]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.cz_func, [target, ctrl1]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.tadj_func, [target]) + self.builder.call(self.t_func, [ctrl1]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.cz_func, [ctrl2, ctrl1]) + self.builder.call(self.h_func, [ctrl1]) + self.builder.call(self.h_func, [target]) + call.erase() + + +Simulator: TypeAlias = Callable[ + [List[QirInstruction], int, int, int, NoiseConfig, int], str +] + + +def preprocess_simulation_input( + input: Union[QirInputData, str, bytes], + shots: Optional[int] = 1, + noise: Optional[NoiseConfig] = None, + seed: Optional[int] = None, +) -> tuple[pyqir.Module, int, Optional[NoiseConfig], int]: + if shots is None: + shots = 1 + # If no seed specified, generate a random u32 to use + if seed is None: + seed = random.randint(0, 2**32 - 1) + if isinstance(noise, tuple): + raise ValueError( + "Specifying Pauli noise via a tuple is not supported. Use a NoiseConfig instead." + ) + + context = pyqir.Context() + if isinstance(input, QirInputData): + mod = pyqir.Module.from_ir(context, str(input)) + elif isinstance(input, str): + mod = pyqir.Module.from_ir(context, input) + else: + mod = pyqir.Module.from_bitcode(context, input) + + return (mod, shots, noise, seed) + + +def is_adaptive(mod: pyqir.Module) -> bool: + """Check if the QIR module uses the Adaptive Profile.""" + entry = next(filter(pyqir.is_entry_point, mod.functions), None) + if entry is None: + return False + func_attrs = entry.attributes.func + if "qir_profiles" not in func_attrs: + return False + return func_attrs["qir_profiles"].string_value == "adaptive_profile" + + +def str_to_result(result: str): + match result: + case "0": + return Result.Zero + case "1": + return Result.One + case "L": + return Result.Loss + case _: + raise ValueError(f"Invalid result {result}") + + +def run_base( + rust_run_base_fn: Callable, + mod: pyqir.Module, + shots: int, + noise: Optional[NoiseConfig], + seed: int, +): + """ + Runs a base profile program given a rust simulator. Adds output recording logic. + """ + if noise is None: + gates, num_qubits, num_results = AggregateGatesPass().run(mod) + else: + gates, num_qubits, num_results = CorrelatedNoisePass(noise).run(mod) + recorder = OutputRecordingPass() + recorder.run(mod) + return list( + map( + recorder.process_output, + rust_run_base_fn(gates, num_qubits, num_results, shots, noise, seed), + ) + ) + + +def run_adaptive( + rust_run_adaptive_fn: Callable, + mod: pyqir.Module, + program: AdaptiveProgram, + shots: int, + noise: Optional[NoiseConfig], + seed: int, +): + """ + Runs an adaptive profile program given a rust simulator. Adds output recording logic. + """ + results = rust_run_adaptive_fn(program.as_dict(), shots, noise, seed) + recorder = OutputRecordingPass() + recorder.run(mod) + return list(map(recorder.process_output, results)) + + +def run_qir_clifford( + input: Union[QirInputData, str, bytes], + shots: Optional[int] = 1, + noise: Optional[NoiseConfig] = None, + seed: Optional[int] = None, +) -> List: + mod, shots, noise, seed = preprocess_simulation_input(input, shots, noise, seed) + if is_adaptive(mod): + program = AdaptiveProfilePass(Bytecode.Bit64).run(mod, noise) + return run_adaptive(run_clifford_adaptive, mod, program, shots, noise, seed) + else: + return run_base(run_clifford, mod, shots, noise, seed) + + +def run_qir_cpu( + input: Union[QirInputData, str, bytes], + shots: Optional[int] = 1, + noise: Optional[NoiseConfig] = None, + seed: Optional[int] = None, +) -> List: + mod, shots, noise, seed = preprocess_simulation_input(input, shots, noise, seed) + DecomposeCcxPass().run(mod) + if is_adaptive(mod): + program = AdaptiveProfilePass(Bytecode.Bit64).run(mod, noise) + return run_adaptive(run_cpu_adaptive, mod, program, shots, noise, seed) + else: + return run_base(run_cpu_full_state, mod, shots, noise, seed) + + +def run_qir_gpu( + input: Union[QirInputData, str, bytes], + shots: Optional[int] = 1, + noise: Optional[NoiseConfig] = None, + seed: Optional[int] = None, +) -> List: + mod, shots, noise, seed = preprocess_simulation_input(input, shots, noise, seed) + # Ccx is not support in the GPU simulator, decompose it + DecomposeCcxPass().run(mod) + if is_adaptive(mod): + program = AdaptiveProfilePass(Bytecode.Bit32).run(mod, noise) + return run_adaptive( + run_adaptive_parallel_shots, mod, program, shots, noise, seed + ) + else: + return run_base(run_parallel_shots, mod, shots, noise, seed) + + +def prepare_qir_with_correlated_noise( + input: Union[QirInputData, str, bytes], + noise_tables: List[Tuple[int, str, int]], +) -> Tuple[List[QirInstruction], int, int]: + # Turn the input into a QIR module + mod, _, _, _ = preprocess_simulation_input(input, None, None, None) + + # Ccx is not support in the GPU simulator, decompose it + DecomposeCcxPass().run(mod) + + # Extract the gates including correlated noise instructions + gates, required_num_qubits, required_num_results = GpuCorrelatedNoisePass( + noise_tables + ).run(mod) + + return (gates, required_num_qubits, required_num_results) + + +class GpuSimulator: + """ + Represents a GPU-based QIR simulator. This is a 'full state' simulator that can simulate + quantum programs, including non-Clifford gates, up to a limit of 27 qubits. + """ + + def __init__(self): + self.gpu_context = GpuContext() + self._is_adaptive = False + self._recorder = None + self.tables = None + + def load_noise_tables( + self, + noise_dir: str, + ): + """ + Loads noise tables from the specified directory path. For each .csv file found in the directory, + the noise table is loaded and associated with a unique identifier. The name of the file (without the .csv extension) + is used as the label for the noise table, which should match the QIR instruction that will apply noise using this table. + + If testing various noise models, you may load new noise models at any time by calling this method again + with a different directory path. Previously loaded noise tables will be replaced. The program currently loaded + into the simulator (if any) will remain loaded, but any subsequent calls to `run_shots` will use the newly loaded noise tables. + + Each line of the table should be of the format: "IXYZ,1.345e-4" where IXYZ is a string of Pauli operators + representing the error on each qubit (Z applying to the first qubit argument, Y to the second, etc.), and the second value + is the corresponding error probability for that specific Pauli string. + + Blank lines, lines starting with #, or lines that start with the string "pauli" (i.e., a column header) are ignored. + """ + self.tables = self.gpu_context.load_noise_tables(noise_dir) + + def set_program(self, input: Union[QirInputData, str, bytes]): + """ + Load the QIR program into the GPU simulator, preparing it for execution. You may load and run + multiple programs sequentially by calling this method multiple times before calling `run_shots` + without needing to create a new simulator instance or reloading noise tables. + """ + # Parse the QIR module to detect profile + mod, _, _, _ = preprocess_simulation_input(input, None, None, None) + if is_adaptive(mod): + self._is_adaptive = True + # Build noise_intrinsics dict from loaded noise tables (if any) + noise_intrinsics = None + if self.tables is not None: + noise_intrinsics = {name: table_id for table_id, name, _ in self.tables} + program = AdaptiveProfilePass(Bytecode.Bit32).run( + mod, noise_intrinsics=noise_intrinsics + ) + self.gpu_context.set_adaptive_program(program.as_dict()) + # This is used later for output recording + self._recorder = OutputRecordingPass() + self._recorder.run(mod) + else: + self._is_adaptive = False + self.gates, self.required_num_qubits, self.required_num_results = ( + prepare_qir_with_correlated_noise( + input, self.tables if not self.tables is None else [] + ) + ) + self.gpu_context.set_program( + self.gates, self.required_num_qubits, self.required_num_results + ) + + def run_shots(self, shots: int, seed: Optional[int] = None) -> "GpuShotResults": + """ + Run the loaded QIR program for the specified number of shots, using an optional seed for reproducibility. + If noise is to be applied, ensure that noise has been loaded prior to running shots. + """ + seed = seed if seed is not None else random.randint(0, 2**32 - 1) + if self._is_adaptive: + results = self.gpu_context.run_adaptive_shots(shots, seed=seed) + for i, (shot_ret_code, shot_result) in enumerate( + zip(results["shot_result_codes"], results["shot_results"]) + ): + if shot_ret_code == 0: + # If the ret_code was zero, we do an output recording pass + # on the output. + results["shot_results"][i] = self._recorder.process_output( + shot_result + ) + else: + # If the shot finished with a ret_code other than zero, + # we set the result to `None`. + results["shot_results"][i] = None + return results + return self.gpu_context.run_shots(shots, seed=seed) + + +def run_qir( + input: Union[QirInputData, str, bytes], + shots: Optional[int] = 1, + noise: Optional[NoiseConfig] = None, + seed: Optional[int] = None, + type: Optional[Literal["clifford", "cpu", "gpu"]] = None, +) -> List: + """ + Simulate the given QIR source. + + :param input: The QIR source to simulate. + :param type: The type of simulator to use. + Use ``"clifford"`` if your QIR only contains Clifford gates and measurements. + Use ``"gpu"`` if you have a GPU available in your system. + Use ``"cpu"`` as a fallback option if you don't have a GPU in your system. + If ``None`` (default), the GPU simulator will be tried first, falling back to + CPU if a suitable GPU device could not be located. + :param shots: The number of shots to run. + :param noise: A noise model to use in the simulation. + :param seed: A seed for reproducibility. + :return: A list of measurement results, in the order they happened during the simulation. + :rtype: List + """ + if type is None: + try: + try_create_gpu_adapter() + type = "gpu" + except OSError: + type = "cpu" + + match type: + case "clifford": + return run_qir_clifford(input, shots, noise, seed) + case "cpu": + return run_qir_cpu(input, shots, noise, seed) + case "gpu": + return run_qir_gpu(input, shots, noise, seed) + case _: + raise ValueError(f"Invalid simulator type: {type}") diff --git a/source/qdk_package/qdk/telemetry.py b/source/qdk_package/qdk/telemetry.py new file mode 100644 index 0000000000..4114c69878 --- /dev/null +++ b/source/qdk_package/qdk/telemetry.py @@ -0,0 +1,310 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +This module sends telemetry directly to Azure Monitor using a similar mechanism and +format to the Azure Monitor OpenTelemetry Python SDK. It only supports custom metrics of +type "counter" and "histogram" for now. It's goal is to be minimal in size and dependencies, +and easy to read to understand exactly what data is being sent. + +To use this API, simply call `log_telemetry` with the metric name, value, and any other +optional properties. The telemetry will be batched and sent at a regular intervals (60 sec), +and when the process is about to exit. + +Disable qsharp Python telemetry by setting the environment variable `QSHARP_PYTHON_TELEMETRY=none`. +""" + +import atexit +import json +import locale +import logging +import os +import platform +import time +import urllib.request +import warnings + +from datetime import datetime, timezone +from queue import SimpleQueue, Empty +from threading import Thread +from typing import Any, Dict, Literal, List, TypedDict, Union + +logger = logging.getLogger(__name__) + +QSHARP_VERSION = "0.0.0.dev0" + +AIKEY = os.environ.get("QSHARP_PYTHON_AI_KEY") or "95d25b22-8b6d-448e-9677-78ad4047a95a" +AIURL = ( + os.environ.get("QSHARP_PYTHON_AI_URL") + or "https://westus2-2.in.applicationinsights.azure.com//v2.1/track" +) + +# If explicitly disabled via either environment variable, disable telemetry. This takes precedence. +# If explicitly enabled via either environment variable, enable telemetry. +# Otherwise, enable telemetry only in release builds. +_disable_values = {"0", "false", "disabled", "none"} +_enable_values = {"1", "true", "enabled"} +_env_values = { + (os.environ.get("QSHARP_PYTHON_TELEMETRY") or "").lower(), + (os.environ.get("QDK_PYTHON_TELEMETRY") or "").lower(), +} + +# `&` here is set intersection: it yields the common values between sets. +# `not _env_values & _disable_values` is True iff no disable value is present. +# `bool(_env_values & _enable_values)` is True iff any enable value is present. +TELEMETRY_ENABLED = not _env_values & _disable_values and ( + bool(_env_values & _enable_values) or "dev" not in QSHARP_VERSION +) + +BATCH_INTERVAL_SEC = int(os.environ.get("QSHARP_PYTHON_TELEMETRY_INTERVAL") or 60) + + +# The below is taken from the Azure Monitor Python SDK +def _getlocale() -> str: + try: + with warnings.catch_warnings(): + # Workaround for https://github.com/python/cpython/issues/82986 by continuing to use getdefaultlocale() even though it has been deprecated. + # Ignore the deprecation warnings to reduce noise + warnings.simplefilter("ignore", category=DeprecationWarning) + return locale.getdefaultlocale()[0] or "" + except AttributeError: + # Use this as a fallback if locale.getdefaultlocale() doesn't exist (>Py3.13) + return locale.getlocale()[0] or "" + + +# Minimal device information to include with telemetry +AI_DEVICE_LOCALE = _getlocale() +AI_DEVICE_OS_VERSION = platform.version() + + +class Metric(TypedDict): + """Used internally for objects in the telemetry queue""" + + name: str + value: float + count: int + properties: Dict[str, Any] + type: str + + +class PendingMetric(Metric): + """Used internally to aggregate metrics before sending""" + + min: float + max: float + + +# Maintain a collection of custom metrics to log, stored by metric name with a list entry +# for each unique set of properties per metric name +pending_metrics: Dict[str, List[PendingMetric]] = {} + +# The telemetry queue is used to send telemetry from the main thread to the telemetry thread +# This simplifies any thread-safety concerns, and avoids the need for locks, etc. +telemetry_queue: Any = SimpleQueue() # type 'Any' until we get off Python 3.8 builds + + +def log_telemetry( + name: str, + value: float, + count: int = 1, + properties: Dict[str, Any] = {}, + type: Literal["counter", "histogram"] = "counter", +) -> None: + """ + Logs a custom metric with the name provided. Properties are optional and can be used to + capture additional context about the metric (but should be a relatively static set of values, as + each unique set of properties will be sent as a separate metric and creates a separate 'dimension' + in the backend telemetry store). + + The type can be either 'counter' or 'histogram'. A 'counter' is a simple value that is summed + over time, such as how many times an event occurs, while a 'histogram' is used to track 'quantative' + values, such as the distribution of values over time, e.g., the duration of an operation. + + Example usage for a counter: + + log_telemetry("qir_generated", 1, properties={"profile": "base", "qsharp.version": "1.9.0"}) + + Example usage for a histogram: + + log_telemetry("simulation_duration", 123.45, type="histogram") + + """ + if not TELEMETRY_ENABLED: + return + + obj: Metric = { + "name": name, + "value": value, + "count": count, + "properties": {**properties, "qsharp.version": QSHARP_VERSION}, + "type": type, + } + + logger.debug("Queuing telemetry: %s", obj) + telemetry_queue.put(obj) + + +def _add_to_pending(metric: Metric): + """Used by the telemetry thread to aggregate metrics before sending""" + + if metric["type"] not in ["counter", "histogram"]: + raise Exception("Metric must be of type counter or histogram") + + # Get or create the entry list for this name + name_entries = pending_metrics.setdefault(metric["name"], []) + + # Try to find the entry with matching properties + # This relies on the fact dicts with matching keys/values compare equal in Python + prop_entry = next( + ( + entry + for entry in name_entries + if entry["properties"] == metric["properties"] + ), + None, + ) + if prop_entry is None: + new_entry: PendingMetric = { + **metric, + "min": metric["value"], + "max": metric["value"], + } + name_entries.append(new_entry) + else: + if prop_entry["type"] != metric["type"]: + raise Exception("Cannot mix counter and histogram for the same metric name") + prop_entry["value"] += metric["value"] + prop_entry["count"] += metric["count"] + prop_entry["min"] = min(prop_entry["min"], metric["value"]) + prop_entry["max"] = max(prop_entry["max"], metric["value"]) + + +def _pending_to_payload() -> List[Dict[str, Any]]: + """Converts the pending metrics to the JSON payload for Azure Monitor""" + + result_array: List[Dict[str, Any]] = [] + formatted_time = ( + datetime.now(timezone.utc) + .isoformat(timespec="microseconds") + .replace("+00:00", "Z") + ) + for name in pending_metrics: + for unique_props in pending_metrics[name]: + # The below matches the entry format for Azure Monitor REST API + entry: Dict[str, Any] = { + "ver": 1, + "name": "Microsoft.ApplicationInsights.Metric", + "time": formatted_time, + "sampleRate": 100.0, + "iKey": AIKEY, + "tags": { + "ai.device.locale": AI_DEVICE_LOCALE, + "ai.device.osVersion": AI_DEVICE_OS_VERSION, + }, + "data": { + "baseType": "MetricData", + "baseData": { + "ver": 2, + "metrics": [ + { + "name": unique_props["name"], + "value": unique_props["value"], + "count": unique_props["count"], + } + ], + "properties": unique_props["properties"], + }, + }, + } + # Histogram values differ only in that they have min/max values also + if unique_props["type"] == "histogram": + entry["data"]["baseData"]["metrics"][0]["min"] = unique_props["min"] + entry["data"]["baseData"]["metrics"][0]["max"] = unique_props["max"] + + result_array.append(entry) + + return result_array + + +def _post_telemetry() -> bool: + """Posts the pending telemetry to Azure Monitor""" + + if len(pending_metrics) == 0: + return True + + payload = json.dumps(_pending_to_payload()).encode("utf-8") + logger.debug("Sending telemetry request: %s", payload) + try: + request = urllib.request.Request(AIURL, data=payload, method="POST") + request.add_header("Content-Type", "application/json") + with urllib.request.urlopen(request, timeout=10) as response: + logger.debug("Telemetry response: %s", response.status) + # On a successful post, clear the pending list. (Else they will be included on the next retry) + pending_metrics.clear() + return True + + except Exception: + logger.debug( + "Failed to post telemetry. Pending metrics will be retried at the next interval." + ) + return False + + +# This is the thread that aggregates and posts telemetry at a regular interval. +# The main thread will signal the thread loop to exit when the process is about to exit. +def _telemetry_thread_start(): + next_post_sec: Union[float, None] = None + + def on_metric(msg: Metric): + nonlocal next_post_sec + + # Add to the pending batch to send next + _add_to_pending(msg) + + # Schedule the next post if we don't have one scheduled + if next_post_sec == None: + next_post_sec = time.monotonic() + BATCH_INTERVAL_SEC + + while True: + try: + # Block if no timeout, else wait a maximum of time until the next post is due + timeout: Union[float, None] = None + if next_post_sec: + timeout = max(next_post_sec - time.monotonic(), 0) + msg = telemetry_queue.get(timeout=timeout) + + if msg == "exit": + logger.debug("Exiting telemetry thread") + if not _post_telemetry(): + logger.debug("Failed to post telemetry on exit") + return + else: + on_metric(msg) + # Loop until the queue has been drained. This will cause the 'Empty' exception + # below once the queue is empty and it's time to post + continue + except Empty: + # No more telemetry within timeout, so write what we have pending + _ = _post_telemetry() + + # If we get here, it's after a post attempt. Pending will still have items if the attempt + # failed, so updated the time for the next attempt in that case. + if len(pending_metrics) == 0: + next_post_sec = None + else: + next_post_sec = time.monotonic() + BATCH_INTERVAL_SEC + + +# When the process is about to exit, notify the telemetry thread to flush, and wait max 3 sec before exiting anyway +def _on_exit(): + logger.debug("In on_exit handler") + telemetry_queue.put("exit") + # Wait at most 3 seconds for the telemetry thread to flush and exit + telemetry_thread.join(timeout=3) + + +# Mark the telemetry thread as a daemon thread, else it will keep the process alive when the main thread exits +if TELEMETRY_ENABLED: + telemetry_thread = Thread(target=_telemetry_thread_start, daemon=True) + telemetry_thread.start() + atexit.register(_on_exit) diff --git a/source/qdk_package/qdk/telemetry_events.py b/source/qdk_package/qdk/telemetry_events.py new file mode 100644 index 0000000000..edffb17585 --- /dev/null +++ b/source/qdk_package/qdk/telemetry_events.py @@ -0,0 +1,357 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from .telemetry import log_telemetry +import math +from typing import Union + +# For metrics such as duration, we want to capture things like how many shots or qubits in +# the additional properties. However properties shouldn't be 'continuous' values, as they +# create new 'dimensions' on the backend, which is limited, thus we want to bucket these properties. + +# See some of the notes at: https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/metrics-custom-overview#design-limitations-and-considerations + + +def get_next_power_of_ten_bucket(value: int) -> int: + if value <= 1: + return 1 + elif value >= 1000000: + # Limit the buckets upper bound + return 1000000 + else: + # Bucket into nearest (rounded up) power of 10, e.g. 75 -> 100, 450 -> 1000, etc. + return 10 ** math.ceil(math.log10(value)) + + +# gets the order of magnitude for the number of qubits +def get_qubits_bucket(qubits: Union[str, int]) -> str: + if qubits == "unknown": + return "unknown" + qubits = int(qubits) + if qubits <= 1: + return "1" + elif qubits >= 50: + return "50" + else: + # integer divide by 5 to get nearest 5 + return str(qubits // 5 * 5) + + +def on_import() -> None: + log_telemetry("qsharp.import", 1) + + +def on_qdk_import() -> None: + log_telemetry("qdk.import", 1) + + +def on_run(shots: int, noise: bool, qubit_loss: bool) -> None: + log_telemetry( + "qsharp.run", + 1, + properties={ + "shots": get_next_power_of_ten_bucket(shots), + "noise": noise, + "qubit_loss": qubit_loss, + }, + ) + + +def on_run_end(durationMs: float, shots: int) -> None: + log_telemetry( + "qsharp.run.durationMs", + durationMs, + properties={"shots": get_next_power_of_ten_bucket(shots)}, + type="histogram", + ) + + +def on_run_qasm(shots: int, noise: bool, qubit_loss: bool) -> None: + log_telemetry( + "qsharp.run_qasm", + 1, + properties={ + "shots": get_next_power_of_ten_bucket(shots), + "noise": noise, + "qubit_loss": qubit_loss, + }, + ) + + +def on_run_qasm_end(durationMs: float, shots: int) -> None: + log_telemetry( + "qsharp.run_qasm.durationMs", + durationMs, + properties={"shots": get_next_power_of_ten_bucket(shots)}, + type="histogram", + ) + + +def on_eval() -> None: + log_telemetry( + "qsharp.eval", + 1, + ) + + +def on_eval_end(durationMs: float) -> None: + log_telemetry( + "qsharp.eval.durationMs", + durationMs, + type="histogram", + ) + + +def on_import_qasm() -> None: + log_telemetry( + "qsharp.import_qasm", + 1, + ) + + +def on_import_qasm_end(durationMs: float) -> None: + log_telemetry( + "qsharp.import_qasm.durationMs", + durationMs, + type="histogram", + ) + + +def on_run_cell() -> None: + log_telemetry( + "qsharp.run.cell", + 1, + ) + + +def on_run_cell_end(durationMs: float) -> None: + log_telemetry( + "qsharp.run.cell.durationMs", + durationMs, + type="histogram", + ) + + +def on_compile(profile: str) -> None: + log_telemetry("qsharp.compile", 1, properties={"profile": profile}) + + +def on_compile_end(durationMs: float, profile: str) -> None: + log_telemetry( + "qsharp.compile.durationMs", + durationMs, + properties={"profile": profile}, + type="histogram", + ) + + +def on_compile_qasm(profile: str) -> None: + log_telemetry("qsharp.compile_qasm", 1, properties={"profile": profile}) + + +def on_compile_qasm_end(durationMs: float, profile: str) -> None: + log_telemetry( + "qsharp.compile_qasm.durationMs", + durationMs, + properties={"profile": profile}, + type="histogram", + ) + + +def on_estimate() -> None: + log_telemetry( + "qsharp.estimate", + 1, + ) + + +def on_estimate_end(durationMs: float, qubits: Union[str, int]) -> None: + log_telemetry( + "qsharp.estimate.durationMs", + durationMs, + properties={"qubits": get_qubits_bucket(qubits)}, + type="histogram", + ) + + +def on_estimate_qasm() -> None: + log_telemetry( + "qsharp.estimate_qasm", + 1, + ) + + +def on_estimate_qasm_end(durationMs: float, qubits: Union[str, int]) -> None: + log_telemetry( + "qsharp.estimate_qasm.durationMs", + durationMs, + properties={"qubits": get_qubits_bucket(qubits)}, + type="histogram", + ) + + +def on_circuit() -> None: + log_telemetry( + "qsharp.circuit", + 1, + ) + + +def on_circuit_end(durationMs: float) -> None: + log_telemetry( + "qsharp.circuit.durationMs", + durationMs, + type="histogram", + ) + + +def on_circuit_qasm() -> None: + log_telemetry( + "qsharp.circuit_qasm", + 1, + ) + + +def on_circuit_qasm_end(durationMs: float) -> None: + log_telemetry( + "qsharp.circuit_qasm.durationMs", + durationMs, + type="histogram", + ) + + +# Qiskit telemetry events + + +def on_qiskit_run(shots: int, num_circuits: int) -> None: + log_telemetry( + "qiskit.run", + 1, + properties={ + "shots": get_next_power_of_ten_bucket(shots), + "circuits": get_next_power_of_ten_bucket(num_circuits), + }, + ) + + +def on_qiskit_run_end(shots: int, num_circuits: int, duration_ms: float) -> None: + log_telemetry( + "qiskit.run.durationMs", + duration_ms, + properties={ + "shots": get_next_power_of_ten_bucket(shots), + "circuits": get_next_power_of_ten_bucket(num_circuits), + }, + type="histogram", + ) + + +def on_qiskit_run_re() -> None: + log_telemetry( + "qiskit.run.re", + 1, + ) + + +def on_qiskit_run_re_end(duration_ms: float) -> None: + log_telemetry( + "qiskit.run.re.durationMs", + duration_ms, + type="histogram", + ) + + +def on_neutral_atom_init(default_layout: bool) -> None: + log_telemetry( + "neutral_atom.device.init", + 1, + properties={"default_layout": default_layout}, + ) + + +def on_neutral_atom_compile() -> None: + log_telemetry( + "neutral_atom.device.compile", + 1, + ) + + +def on_neutral_atom_compile_end(duration_ms: float) -> None: + log_telemetry( + "neutral_atom.device.compile.durationMs", + duration_ms, + type="histogram", + ) + + +def on_neutral_atom_trace() -> None: + log_telemetry( + "neutral_atom.device.trace", + 1, + ) + + +def on_neutral_atom_trace_end(duration_ms: float) -> None: + log_telemetry( + "neutral_atom.device.trace.durationMs", + duration_ms, + type="histogram", + ) + + +def on_neutral_atom_cpu_fallback() -> None: + log_telemetry( + "neutral_atom.device.cpu_fallback", + 1, + ) + + +def on_neutral_atom_simulate(shots: int, noise: bool, type: str) -> None: + log_telemetry( + "neutral_atom.device.simulate", + 1, + properties={ + "shots": get_next_power_of_ten_bucket(shots), + "noise": noise, + "type": type, + }, + ) + + +def on_neutral_atom_simulate_end( + duration_ms: float, shots: int, noise: bool, type: str +) -> None: + log_telemetry( + "neutral_atom.device.simulate.durationMs", + duration_ms, + properties={ + "shots": get_next_power_of_ten_bucket(shots), + "noise": noise, + "type": type, + }, + type="histogram", + ) + + +# QRE telemetry events + + +def on_qre_estimate(post_process: bool, use_graph: bool) -> None: + log_telemetry( + "qsharp.qre.estimate", + 1, + properties={ + "post_process": post_process, + "use_graph": use_graph, + }, + ) + + +def on_qre_application_created(application_type: str) -> None: + log_telemetry( + "qsharp.qre.application.created", + 1, + properties={ + "application_type": application_type, + }, + ) diff --git a/source/pip/src/displayable_output.rs b/source/qdk_package/src/displayable_output.rs similarity index 100% rename from source/pip/src/displayable_output.rs rename to source/qdk_package/src/displayable_output.rs diff --git a/source/pip/src/displayable_output/tests.rs b/source/qdk_package/src/displayable_output/tests.rs similarity index 100% rename from source/pip/src/displayable_output/tests.rs rename to source/qdk_package/src/displayable_output/tests.rs diff --git a/source/pip/src/fs.rs b/source/qdk_package/src/fs.rs similarity index 100% rename from source/pip/src/fs.rs rename to source/qdk_package/src/fs.rs diff --git a/source/pip/src/generic_estimator.rs b/source/qdk_package/src/generic_estimator.rs similarity index 100% rename from source/pip/src/generic_estimator.rs rename to source/qdk_package/src/generic_estimator.rs diff --git a/source/pip/src/generic_estimator/code.rs b/source/qdk_package/src/generic_estimator/code.rs similarity index 100% rename from source/pip/src/generic_estimator/code.rs rename to source/qdk_package/src/generic_estimator/code.rs diff --git a/source/pip/src/generic_estimator/counts.rs b/source/qdk_package/src/generic_estimator/counts.rs similarity index 100% rename from source/pip/src/generic_estimator/counts.rs rename to source/qdk_package/src/generic_estimator/counts.rs diff --git a/source/pip/src/generic_estimator/factory.rs b/source/qdk_package/src/generic_estimator/factory.rs similarity index 100% rename from source/pip/src/generic_estimator/factory.rs rename to source/qdk_package/src/generic_estimator/factory.rs diff --git a/source/pip/src/generic_estimator/factory/dispatch.rs b/source/qdk_package/src/generic_estimator/factory/dispatch.rs similarity index 100% rename from source/pip/src/generic_estimator/factory/dispatch.rs rename to source/qdk_package/src/generic_estimator/factory/dispatch.rs diff --git a/source/pip/src/generic_estimator/factory/round_based.rs b/source/qdk_package/src/generic_estimator/factory/round_based.rs similarity index 100% rename from source/pip/src/generic_estimator/factory/round_based.rs rename to source/qdk_package/src/generic_estimator/factory/round_based.rs diff --git a/source/pip/src/generic_estimator/tests.rs b/source/qdk_package/src/generic_estimator/tests.rs similarity index 100% rename from source/pip/src/generic_estimator/tests.rs rename to source/qdk_package/src/generic_estimator/tests.rs diff --git a/source/pip/src/generic_estimator/utils.rs b/source/qdk_package/src/generic_estimator/utils.rs similarity index 100% rename from source/pip/src/generic_estimator/utils.rs rename to source/qdk_package/src/generic_estimator/utils.rs diff --git a/source/pip/src/interop.rs b/source/qdk_package/src/interop.rs similarity index 100% rename from source/pip/src/interop.rs rename to source/qdk_package/src/interop.rs diff --git a/source/pip/src/interpreter.rs b/source/qdk_package/src/interpreter.rs similarity index 99% rename from source/pip/src/interpreter.rs rename to source/qdk_package/src/interpreter.rs index 5605bb5d9c..a2ca8f10bc 100644 --- a/source/pip/src/interpreter.rs +++ b/source/qdk_package/src/interpreter.rs @@ -157,7 +157,7 @@ fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> { // This ordering must match the _native.pyi file. #[derive(Clone, Copy, Default, PartialEq)] -#[pyclass(eq, eq_int, from_py_object, module = "qsharp._native")] +#[pyclass(eq, eq_int, from_py_object, module = "qdk._native")] #[allow(non_camel_case_types)] /// A Q# target profile. /// @@ -264,7 +264,7 @@ impl From for Profile { // This ordering must match the _native.pyi file. #[derive(Clone, Copy, Default, PartialEq)] -#[pyclass(eq, eq_int, from_py_object, module = "qsharp._native")] +#[pyclass(eq, eq_int, from_py_object, module = "qdk._native")] #[allow(non_camel_case_types)] /// Represents the output semantics for OpenQASM 3 compilation. /// Each has implications on the output of the compilation @@ -328,7 +328,7 @@ impl From for qsc::openqasm::OutputSemantics { // This ordering must match the _native.pyi file. #[derive(Clone, Copy, Default, PartialEq)] -#[pyclass(eq, eq_int, from_py_object, module = "qsharp._native")] +#[pyclass(eq, eq_int, from_py_object, module = "qdk._native")] #[allow(non_camel_case_types)] /// Represents the type of compilation output to create pub enum ProgramType { diff --git a/source/pip/src/interpreter/data_interop.rs b/source/qdk_package/src/interpreter/data_interop.rs similarity index 99% rename from source/pip/src/interpreter/data_interop.rs rename to source/qdk_package/src/interpreter/data_interop.rs index 8d052c44c3..f2ec59a090 100644 --- a/source/pip/src/interpreter/data_interop.rs +++ b/source/qdk_package/src/interpreter/data_interop.rs @@ -25,7 +25,7 @@ use std::rc::Rc; /// Instances of this enum represent a Q# type. This is used /// to send the definitions of Q# UDTs defined by the user to Python -/// and creating equivalent Python dataclasses in `qsharp.code.*`. +/// and creating equivalent Python dataclasses in `qdk.code.*`. #[pyclass(from_py_object)] #[derive(Clone)] pub(super) enum TypeIR { diff --git a/source/pip/src/lib.rs b/source/qdk_package/src/lib.rs similarity index 100% rename from source/pip/src/lib.rs rename to source/qdk_package/src/lib.rs diff --git a/source/pip/src/noisy_simulator.rs b/source/qdk_package/src/noisy_simulator.rs similarity index 100% rename from source/pip/src/noisy_simulator.rs rename to source/qdk_package/src/noisy_simulator.rs diff --git a/source/pip/src/qir_simulation.rs b/source/qdk_package/src/qir_simulation.rs similarity index 99% rename from source/pip/src/qir_simulation.rs rename to source/qdk_package/src/qir_simulation.rs index 555d97798d..42741a4065 100644 --- a/source/pip/src/qir_simulation.rs +++ b/source/qdk_package/src/qir_simulation.rs @@ -72,7 +72,7 @@ pub enum QirInstructionId { } #[derive(Debug)] -#[pyclass(module = "qsharp._native")] +#[pyclass(module = "qdk._native")] #[derive(FromPyObject)] pub enum QirInstruction { OneQubitGate(QirInstructionId, u32), @@ -89,7 +89,7 @@ pub enum QirInstruction { } #[derive(Debug)] -#[pyclass(module = "qsharp._native")] +#[pyclass(module = "qdk._native")] pub struct NoiseConfig { #[pyo3(get)] pub i: Py, @@ -312,7 +312,7 @@ pub(crate) fn unbind_noise_config( } #[derive(Clone, Copy, Debug)] -#[pyclass(from_py_object, module = "qsharp._native")] +#[pyclass(from_py_object, module = "qdk._native")] pub struct IdleNoiseParams { #[pyo3(get, set)] pub s_probability: Probability, @@ -354,7 +354,7 @@ impl From for IdleNoiseParams { } #[derive(Clone, Debug)] -#[pyclass(from_py_object, module = "qsharp._native")] +#[pyclass(from_py_object, module = "qdk._native")] pub struct NoiseTable { qubits: u32, pauli_noise: FxHashMap, @@ -673,7 +673,7 @@ impl From> for NoiseTable } #[derive(Debug, Default)] -#[pyclass(from_py_object, module = "qsharp._native")] +#[pyclass(from_py_object, module = "qdk._native")] pub struct NoiseIntrinsicsTable { next_id: u32, table: FxHashMap)>, diff --git a/source/pip/src/qir_simulation/correlated_noise.rs b/source/qdk_package/src/qir_simulation/correlated_noise.rs similarity index 100% rename from source/pip/src/qir_simulation/correlated_noise.rs rename to source/qdk_package/src/qir_simulation/correlated_noise.rs diff --git a/source/pip/src/qir_simulation/correlated_noise/tests.rs b/source/qdk_package/src/qir_simulation/correlated_noise/tests.rs similarity index 100% rename from source/pip/src/qir_simulation/correlated_noise/tests.rs rename to source/qdk_package/src/qir_simulation/correlated_noise/tests.rs diff --git a/source/pip/src/qir_simulation/cpu_simulators.rs b/source/qdk_package/src/qir_simulation/cpu_simulators.rs similarity index 100% rename from source/pip/src/qir_simulation/cpu_simulators.rs rename to source/qdk_package/src/qir_simulation/cpu_simulators.rs diff --git a/source/pip/src/qir_simulation/gpu_full_state.rs b/source/qdk_package/src/qir_simulation/gpu_full_state.rs similarity index 99% rename from source/pip/src/qir_simulation/gpu_full_state.rs rename to source/qdk_package/src/qir_simulation/gpu_full_state.rs index e0aa4ac7be..a2816ccf2f 100644 --- a/source/pip/src/qir_simulation/gpu_full_state.rs +++ b/source/qdk_package/src/qir_simulation/gpu_full_state.rs @@ -97,7 +97,7 @@ pub fn run_parallel_shots<'py>( type NativeGpuContext = gpu_context::GpuContext; #[derive(Debug)] -#[pyclass(module = "qsharp._native")] +#[pyclass(module = "qdk._native")] pub struct GpuContext { native_context: Mutex, last_set_result_count: usize, // Needed to format results diff --git a/source/pip/src/qre.rs b/source/qdk_package/src/qre.rs similarity index 100% rename from source/pip/src/qre.rs rename to source/qdk_package/src/qre.rs diff --git a/source/pip/src/state_header_template.html b/source/qdk_package/src/state_header_template.html similarity index 100% rename from source/pip/src/state_header_template.html rename to source/qdk_package/src/state_header_template.html diff --git a/source/pip/src/state_row_template.html b/source/qdk_package/src/state_row_template.html similarity index 100% rename from source/pip/src/state_row_template.html rename to source/qdk_package/src/state_row_template.html diff --git a/source/qdk_package/test_requirements.txt b/source/qdk_package/test_requirements.txt index e13e58d3f3..eb3a1d28a5 100644 --- a/source/qdk_package/test_requirements.txt +++ b/source/qdk_package/test_requirements.txt @@ -1,2 +1,5 @@ pytest +expecttest==0.3.0 pyqir>=0.12.3,<0.13 +cirq==1.6.1; platform_system != 'Windows' or platform_machine == 'AMD64' +pandas>=2.1 diff --git a/source/pip/tests-integration/.gitignore b/source/qdk_package/tests-integration/.gitignore similarity index 100% rename from source/pip/tests-integration/.gitignore rename to source/qdk_package/tests-integration/.gitignore diff --git a/source/pip/tests-integration/__init__.py b/source/qdk_package/tests-integration/__init__.py similarity index 70% rename from source/pip/tests-integration/__init__.py rename to source/qdk_package/tests-integration/__init__.py index d84a4b96d5..4af6a30469 100644 --- a/source/pip/tests-integration/__init__.py +++ b/source/qdk_package/tests-integration/__init__.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -"""qsharp integration tests""" +"""qdk integration tests""" diff --git a/source/qdk_package/tests-integration/conftest.py b/source/qdk_package/tests-integration/conftest.py new file mode 100644 index 0000000000..315c5c2143 --- /dev/null +++ b/source/qdk_package/tests-integration/conftest.py @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +This file is used to configure pytest for the test suite. + +- It attempts to import necessary modules from test_circuits. +- It monkey-patches extra symbols onto the ``qdk`` module so that + ``import qdk as qsharp`` followed by ``qsharp.compile(...)`` etc. works. + +Fixtures and other configurations for pytest can be added to this file to +be shared across multiple test files. +""" + +# --------------------------------------------------------------------------- +# Monkey-patch symbols onto qdk that are NOT part of the public API but are +# used throughout tests via the ``import qdk as qsharp`` alias. +# --------------------------------------------------------------------------- +import qdk +from qdk._interpreter import ( # noqa: E402 + eval, + run, + compile, + circuit, + estimate, + logical_counts, +) +from qdk._native import QSharpError, CircuitGenerationMethod, estimate_custom # type: ignore # noqa: E402 + +qdk.eval = eval +qdk.run = run +qdk.compile = compile +qdk.circuit = circuit +qdk.estimate = estimate +qdk.logical_counts = logical_counts +qdk.QSharpError = QSharpError +qdk.CircuitGenerationMethod = CircuitGenerationMethod +qdk.estimate_custom = estimate_custom + +from interop_qiskit.test_circuits import * diff --git a/source/pip/tests-integration/devices/__init__.py b/source/qdk_package/tests-integration/devices/__init__.py similarity index 100% rename from source/pip/tests-integration/devices/__init__.py rename to source/qdk_package/tests-integration/devices/__init__.py diff --git a/source/pip/tests-integration/devices/test_atom_decomp.py b/source/qdk_package/tests-integration/devices/test_atom_decomp.py similarity index 99% rename from source/pip/tests-integration/devices/test_atom_decomp.py rename to source/qdk_package/tests-integration/devices/test_atom_decomp.py index 6b71011bb5..10db7d0f95 100644 --- a/source/pip/tests-integration/devices/test_atom_decomp.py +++ b/source/qdk_package/tests-integration/devices/test_atom_decomp.py @@ -4,8 +4,8 @@ import pytest from expecttest import assert_expected_inline -import qsharp -from qsharp._device._atom._decomp import ( +import qdk as qsharp +from qdk._device._atom._decomp import ( DecomposeMultiQubitToCZ, DecomposeSingleRotationToRz, DecomposeSingleQubitToRzSX, diff --git a/source/pip/tests-integration/devices/test_atom_e2e.py b/source/qdk_package/tests-integration/devices/test_atom_e2e.py similarity index 98% rename from source/pip/tests-integration/devices/test_atom_e2e.py rename to source/qdk_package/tests-integration/devices/test_atom_e2e.py index 1a7514e262..2e6bc4d16f 100644 --- a/source/pip/tests-integration/devices/test_atom_e2e.py +++ b/source/qdk_package/tests-integration/devices/test_atom_e2e.py @@ -4,8 +4,9 @@ import pytest from expecttest import assert_expected_inline -import qsharp -from qsharp._device._atom import NeutralAtomDevice, NoiseConfig +import qdk as qsharp +from qdk._device._atom import NeutralAtomDevice +from qdk.simulation import NoiseConfig try: import pyqir diff --git a/source/pip/tests-integration/devices/test_atom_optimize.py b/source/qdk_package/tests-integration/devices/test_atom_optimize.py similarity index 99% rename from source/pip/tests-integration/devices/test_atom_optimize.py rename to source/qdk_package/tests-integration/devices/test_atom_optimize.py index cc3a26f5cb..39b0992a26 100644 --- a/source/pip/tests-integration/devices/test_atom_optimize.py +++ b/source/qdk_package/tests-integration/devices/test_atom_optimize.py @@ -4,8 +4,8 @@ import pytest from expecttest import assert_expected_inline -import qsharp -from qsharp._device._atom._optimize import ( +import qdk as qsharp +from qdk._device._atom._optimize import ( PruneUnusedFunctions, OptimizeSingleQubitGates, ) diff --git a/source/pip/tests-integration/devices/test_atom_reorder.py b/source/qdk_package/tests-integration/devices/test_atom_reorder.py similarity index 99% rename from source/pip/tests-integration/devices/test_atom_reorder.py rename to source/qdk_package/tests-integration/devices/test_atom_reorder.py index ee8bacfc93..b1fc7d0198 100644 --- a/source/pip/tests-integration/devices/test_atom_reorder.py +++ b/source/qdk_package/tests-integration/devices/test_atom_reorder.py @@ -4,9 +4,9 @@ import pytest from expecttest import assert_expected_inline -import qsharp -from qsharp._device._atom._reorder import Reorder -from qsharp._device._atom import NeutralAtomDevice +import qdk as qsharp +from qdk._device._atom._reorder import Reorder +from qdk._device._atom import NeutralAtomDevice from .validation import PerQubitOrdering, check_qubit_ordering_unchanged try: diff --git a/source/pip/tests-integration/devices/test_atom_schedule.py b/source/qdk_package/tests-integration/devices/test_atom_schedule.py similarity index 99% rename from source/pip/tests-integration/devices/test_atom_schedule.py rename to source/qdk_package/tests-integration/devices/test_atom_schedule.py index c90ec91daa..ce98268d01 100644 --- a/source/pip/tests-integration/devices/test_atom_schedule.py +++ b/source/qdk_package/tests-integration/devices/test_atom_schedule.py @@ -4,9 +4,9 @@ import pytest from expecttest import assert_expected_inline -import qsharp -from qsharp._device._atom import NeutralAtomDevice -from qsharp._device._atom._scheduler import Schedule +import qdk as qsharp +from qdk._device._atom import NeutralAtomDevice +from qdk._device._atom._scheduler import Schedule from .validation import ( ValidateBeginEndParallel, PerQubitOrdering, diff --git a/source/pip/tests-integration/devices/validation/__init__.py b/source/qdk_package/tests-integration/devices/validation/__init__.py similarity index 100% rename from source/pip/tests-integration/devices/validation/__init__.py rename to source/qdk_package/tests-integration/devices/validation/__init__.py diff --git a/source/pip/tests-integration/interop_cirq/__init__.py b/source/qdk_package/tests-integration/interop_cirq/__init__.py similarity index 100% rename from source/pip/tests-integration/interop_cirq/__init__.py rename to source/qdk_package/tests-integration/interop_cirq/__init__.py diff --git a/source/pip/tests-integration/interop_cirq/test_neutral_atom.py b/source/qdk_package/tests-integration/interop_cirq/test_neutral_atom.py similarity index 98% rename from source/pip/tests-integration/interop_cirq/test_neutral_atom.py rename to source/qdk_package/tests-integration/interop_cirq/test_neutral_atom.py index ab09bfe5ed..e69321185e 100644 --- a/source/pip/tests-integration/interop_cirq/test_neutral_atom.py +++ b/source/qdk_package/tests-integration/interop_cirq/test_neutral_atom.py @@ -6,9 +6,9 @@ cirq = pytest.importorskip("cirq") import numpy as np -from qsharp.interop.cirq import NeutralAtomCirqResult, NeutralAtomSampler -from qsharp._simulation import NoiseConfig -from qsharp._device._atom import NeutralAtomDevice +from qdk.cirq import NeutralAtomCirqResult, NeutralAtomSampler +from qdk.simulation import NoiseConfig +from qdk._device._atom import NeutralAtomDevice # --------------------------------------------------------------------------- diff --git a/source/pip/tests-integration/interop_qiskit/__init__.py b/source/qdk_package/tests-integration/interop_qiskit/__init__.py similarity index 100% rename from source/pip/tests-integration/interop_qiskit/__init__.py rename to source/qdk_package/tests-integration/interop_qiskit/__init__.py diff --git a/source/pip/tests-integration/interop_qiskit/resources/custom_intrinsics.inc b/source/qdk_package/tests-integration/interop_qiskit/resources/custom_intrinsics.inc similarity index 100% rename from source/pip/tests-integration/interop_qiskit/resources/custom_intrinsics.inc rename to source/qdk_package/tests-integration/interop_qiskit/resources/custom_intrinsics.inc diff --git a/source/pip/tests-integration/interop_qiskit/resources/custom_intrinsics.ll b/source/qdk_package/tests-integration/interop_qiskit/resources/custom_intrinsics.ll similarity index 100% rename from source/pip/tests-integration/interop_qiskit/resources/custom_intrinsics.ll rename to source/qdk_package/tests-integration/interop_qiskit/resources/custom_intrinsics.ll diff --git a/source/pip/tests-integration/interop_qiskit/test_circuits/__init__.py b/source/qdk_package/tests-integration/interop_qiskit/test_circuits/__init__.py similarity index 100% rename from source/pip/tests-integration/interop_qiskit/test_circuits/__init__.py rename to source/qdk_package/tests-integration/interop_qiskit/test_circuits/__init__.py diff --git a/source/pip/tests-integration/interop_qiskit/test_circuits/test_circuits.py b/source/qdk_package/tests-integration/interop_qiskit/test_circuits/test_circuits.py similarity index 99% rename from source/pip/tests-integration/interop_qiskit/test_circuits/test_circuits.py rename to source/qdk_package/tests-integration/interop_qiskit/test_circuits/test_circuits.py index 00fea5ac06..8745ef18ee 100644 --- a/source/pip/tests-integration/interop_qiskit/test_circuits/test_circuits.py +++ b/source/qdk_package/tests-integration/interop_qiskit/test_circuits/test_circuits.py @@ -10,7 +10,7 @@ if QISKIT_AVAILABLE: from qiskit.circuit import QuantumCircuit - from qsharp.interop.qiskit import QSharpBackend + from qdk.qiskit import QSharpBackend def random_bit() -> Tuple["QuantumCircuit", List[str]]: diff --git a/source/pip/tests-integration/interop_qiskit/test_gate_correctness.py b/source/qdk_package/tests-integration/interop_qiskit/test_gate_correctness.py similarity index 97% rename from source/pip/tests-integration/interop_qiskit/test_gate_correctness.py rename to source/qdk_package/tests-integration/interop_qiskit/test_gate_correctness.py index 6cca737bd8..cfdfe77997 100644 --- a/source/pip/tests-integration/interop_qiskit/test_gate_correctness.py +++ b/source/qdk_package/tests-integration/interop_qiskit/test_gate_correctness.py @@ -2,16 +2,16 @@ # Licensed under the MIT License. import pytest -from qsharp import TargetProfile +from qdk import TargetProfile from interop_qiskit import QISKIT_AVAILABLE, SKIP_REASON if QISKIT_AVAILABLE: from qiskit import QuantumCircuit from qiskit.qasm3 import dumps - from qsharp.interop.qiskit import QSharpBackend - from qsharp.openqasm import run as run_qasm, OutputSemantics - from qsharp import set_quantum_seed, init + from qdk.qiskit import QSharpBackend + from qdk.openqasm import run as run_qasm, OutputSemantics + from qdk import set_quantum_seed, init from .test_circuits import ( generate_repro_information, diff --git a/source/pip/tests-integration/interop_qiskit/test_gateset_qasm.py b/source/qdk_package/tests-integration/interop_qiskit/test_gateset_qasm.py similarity index 99% rename from source/pip/tests-integration/interop_qiskit/test_gateset_qasm.py rename to source/qdk_package/tests-integration/interop_qiskit/test_gateset_qasm.py index 458a5e29c5..861040989b 100644 --- a/source/pip/tests-integration/interop_qiskit/test_gateset_qasm.py +++ b/source/qdk_package/tests-integration/interop_qiskit/test_gateset_qasm.py @@ -8,7 +8,7 @@ if QISKIT_AVAILABLE: from qiskit import QuantumCircuit - from qsharp.interop.qiskit import QSharpBackend + from qdk.qiskit import QSharpBackend def run_transpile_test( diff --git a/source/pip/tests-integration/interop_qiskit/test_neutral_atom.py b/source/qdk_package/tests-integration/interop_qiskit/test_neutral_atom.py similarity index 98% rename from source/pip/tests-integration/interop_qiskit/test_neutral_atom.py rename to source/qdk_package/tests-integration/interop_qiskit/test_neutral_atom.py index b384cb33d5..89aa66bbd1 100644 --- a/source/pip/tests-integration/interop_qiskit/test_neutral_atom.py +++ b/source/qdk_package/tests-integration/interop_qiskit/test_neutral_atom.py @@ -11,9 +11,9 @@ from qiskit import ClassicalRegister from qiskit.circuit import QuantumCircuit from qiskit.providers import JobStatus - from qsharp.interop.qiskit import NeutralAtomBackend - from qsharp._simulation import NoiseConfig - from qsharp._device._atom import NeutralAtomDevice + from qdk.qiskit import NeutralAtomBackend + from qdk.simulation import NoiseConfig + from qdk._device._atom import NeutralAtomDevice from .test_circuits import generate_repro_information @@ -454,7 +454,7 @@ def test_pretranspiled_matches_backend_transpiled(backend) -> None: @pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) def test_non_base_target_profile_raises(backend) -> None: """Passing a non-Base target_profile must raise ValueError immediately.""" - from qsharp import TargetProfile + from qdk import TargetProfile circuit = create_bell_circuit() with pytest.raises(ValueError, match="TargetProfile.Base"): diff --git a/source/pip/tests-integration/interop_qiskit/test_qir.py b/source/qdk_package/tests-integration/interop_qiskit/test_qir.py similarity index 98% rename from source/pip/tests-integration/interop_qiskit/test_qir.py rename to source/qdk_package/tests-integration/interop_qiskit/test_qir.py index c359b7220d..f7194ac58a 100644 --- a/source/pip/tests-integration/interop_qiskit/test_qir.py +++ b/source/qdk_package/tests-integration/interop_qiskit/test_qir.py @@ -5,7 +5,8 @@ from typing import Optional import pytest -from qsharp import TargetProfile, QSharpError +from qdk import TargetProfile +from qdk._native import QSharpError from . import QISKIT_AVAILABLE, SKIP_REASON, ignore_on_failure @@ -14,7 +15,7 @@ from .test_circuits import ( generate_repro_information, ) - from qsharp.interop.qiskit import ( + from qdk.qiskit import ( OutputSemantics, QSharpBackend, QasmError, diff --git a/source/pip/tests-integration/interop_qiskit/test_qsharp.py b/source/qdk_package/tests-integration/interop_qiskit/test_qsharp.py similarity index 97% rename from source/pip/tests-integration/interop_qiskit/test_qsharp.py rename to source/qdk_package/tests-integration/interop_qiskit/test_qsharp.py index 62d0511e59..6f86598935 100644 --- a/source/pip/tests-integration/interop_qiskit/test_qsharp.py +++ b/source/qdk_package/tests-integration/interop_qiskit/test_qsharp.py @@ -7,7 +7,7 @@ if QISKIT_AVAILABLE: - from qsharp.interop.qiskit import ( + from qdk.qiskit import ( OutputSemantics, ProgramType, QSharpBackend, diff --git a/source/pip/tests-integration/interop_qiskit/test_re.py b/source/qdk_package/tests-integration/interop_qiskit/test_re.py similarity index 97% rename from source/pip/tests-integration/interop_qiskit/test_re.py rename to source/qdk_package/tests-integration/interop_qiskit/test_re.py index f87df50bfa..30501fbfab 100644 --- a/source/pip/tests-integration/interop_qiskit/test_re.py +++ b/source/qdk_package/tests-integration/interop_qiskit/test_re.py @@ -4,8 +4,8 @@ from concurrent.futures import ThreadPoolExecutor import pytest -from qsharp import QSharpError -from qsharp.estimator import ( +from qdk._native import QSharpError +from qdk.estimator import ( EstimatorParams, QubitParams, LogicalCounts, @@ -20,7 +20,7 @@ ) from qiskit.circuit import QuantumCircuit, Parameter from qiskit.circuit.library import RGQFTMultiplier - from qsharp.interop.qiskit import ResourceEstimatorBackend + from qdk.qiskit import ResourceEstimatorBackend from qiskit.version import __version__ as QISKIT_VERSION diff --git a/source/pip/tests-integration/interop_qiskit/test_run_sim.py b/source/qdk_package/tests-integration/interop_qiskit/test_run_sim.py similarity index 98% rename from source/pip/tests-integration/interop_qiskit/test_run_sim.py rename to source/qdk_package/tests-integration/interop_qiskit/test_run_sim.py index 627a7d9399..4b2a20da88 100644 --- a/source/pip/tests-integration/interop_qiskit/test_run_sim.py +++ b/source/qdk_package/tests-integration/interop_qiskit/test_run_sim.py @@ -3,7 +3,8 @@ from concurrent.futures import ThreadPoolExecutor import pytest -from qsharp import QSharpError, TargetProfile +from qdk import TargetProfile +from qdk._native import QSharpError from interop_qiskit import QISKIT_AVAILABLE, SKIP_REASON @@ -16,7 +17,7 @@ from qiskit.qasm3 import loads as from_qasm3 from qiskit.providers import JobStatus from qiskit import ClassicalRegister - from qsharp.interop.qiskit import QSharpBackend + from qdk.qiskit import QSharpBackend from .test_circuits import ( generate_repro_information, ) diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/ArithmeticOps.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/ArithmeticOps.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/ArithmeticOps.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/ArithmeticOps.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/BernsteinVaziraniNISQ.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/BernsteinVaziraniNISQ.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/BernsteinVaziraniNISQ.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/BernsteinVaziraniNISQ.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/ConstantFolding.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/ConstantFolding.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/ConstantFolding.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/ConstantFolding.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/CopyAndUpdateExpressions.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/CopyAndUpdateExpressions.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/CopyAndUpdateExpressions.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/CopyAndUpdateExpressions.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/DeutschJozsaNISQ.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/DeutschJozsaNISQ.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/DeutschJozsaNISQ.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/DeutschJozsaNISQ.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/ExpandedTests.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/ExpandedTests.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/ExpandedTests.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/ExpandedTests.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/Functors.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/Functors.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/Functors.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/Functors.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/HiddenShiftNISQ.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/HiddenShiftNISQ.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/HiddenShiftNISQ.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/HiddenShiftNISQ.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/IntegerComparison.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/IntegerComparison.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/IntegerComparison.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/IntegerComparison.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicCCNOT.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicCCNOT.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicCCNOT.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicCCNOT.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicCNOT.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicCNOT.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicCNOT.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicCNOT.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicHIXYZ.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicHIXYZ.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicHIXYZ.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicHIXYZ.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicM.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicM.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicM.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicM.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicMeasureWithBitFlipCode.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicMeasureWithBitFlipCode.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicMeasureWithBitFlipCode.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicMeasureWithBitFlipCode.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicMeasureWithPhaseFlipCode.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicMeasureWithPhaseFlipCode.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicMeasureWithPhaseFlipCode.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicMeasureWithPhaseFlipCode.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicRotationsWithPeriod.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicRotationsWithPeriod.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicRotationsWithPeriod.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicRotationsWithPeriod.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicSTSWAP.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicSTSWAP.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/IntrinsicSTSWAP.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/IntrinsicSTSWAP.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/MeasureAndReuse.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/MeasureAndReuse.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/MeasureAndReuse.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/MeasureAndReuse.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/MeasurementComparison.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/MeasurementComparison.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/MeasurementComparison.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/MeasurementComparison.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/NestedBranching.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/NestedBranching.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/NestedBranching.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/NestedBranching.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/RandomBit.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/RandomBit.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/RandomBit.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/RandomBit.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/SampleTeleport.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/SampleTeleport.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/SampleTeleport.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/SampleTeleport.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/ShortcuttingMeasurement.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/ShortcuttingMeasurement.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/ShortcuttingMeasurement.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/ShortcuttingMeasurement.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/Slicing.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/Slicing.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/Slicing.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/Slicing.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/SuperdenseCoding.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/SuperdenseCoding.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/SuperdenseCoding.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/SuperdenseCoding.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/SwitchHandling.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/SwitchHandling.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/SwitchHandling.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/SwitchHandling.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/ThreeQubitRepetitionCode.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/ThreeQubitRepetitionCode.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/ThreeQubitRepetitionCode.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/ThreeQubitRepetitionCode.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/input/WithinApply.qs b/source/qdk_package/tests-integration/resources/adaptive_ri/input/WithinApply.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/input/WithinApply.qs rename to source/qdk_package/tests-integration/resources/adaptive_ri/input/WithinApply.qs diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/ArithmeticOps.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/ArithmeticOps.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/ArithmeticOps.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/ArithmeticOps.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/ArithmeticOps.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/ArithmeticOps.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/ArithmeticOps.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/ArithmeticOps.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/BernsteinVaziraniNISQ.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/BernsteinVaziraniNISQ.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/BernsteinVaziraniNISQ.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/BernsteinVaziraniNISQ.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/BernsteinVaziraniNISQ.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/BernsteinVaziraniNISQ.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/BernsteinVaziraniNISQ.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/BernsteinVaziraniNISQ.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/ConstantFolding.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/ConstantFolding.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/ConstantFolding.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/ConstantFolding.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/ConstantFolding.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/ConstantFolding.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/ConstantFolding.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/ConstantFolding.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/CopyAndUpdateExpressions.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/CopyAndUpdateExpressions.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/CopyAndUpdateExpressions.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/CopyAndUpdateExpressions.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/CopyAndUpdateExpressions.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/CopyAndUpdateExpressions.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/CopyAndUpdateExpressions.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/CopyAndUpdateExpressions.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/DeutschJozsaNISQ.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/DeutschJozsaNISQ.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/DeutschJozsaNISQ.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/DeutschJozsaNISQ.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/DeutschJozsaNISQ.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/DeutschJozsaNISQ.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/DeutschJozsaNISQ.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/DeutschJozsaNISQ.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/ExpandedTests.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/ExpandedTests.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/ExpandedTests.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/ExpandedTests.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/ExpandedTests.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/ExpandedTests.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/ExpandedTests.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/ExpandedTests.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/Functors.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/Functors.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/Functors.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/Functors.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/Functors.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/Functors.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/Functors.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/Functors.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/HiddenShiftNISQ.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/HiddenShiftNISQ.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/HiddenShiftNISQ.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/HiddenShiftNISQ.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/HiddenShiftNISQ.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/HiddenShiftNISQ.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/HiddenShiftNISQ.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/HiddenShiftNISQ.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntegerComparison.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntegerComparison.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntegerComparison.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntegerComparison.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntegerComparison.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntegerComparison.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntegerComparison.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntegerComparison.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicCCNOT.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicCCNOT.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicCCNOT.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicCCNOT.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicCCNOT.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicCCNOT.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicCCNOT.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicCCNOT.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicCNOT.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicCNOT.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicCNOT.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicCNOT.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicCNOT.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicCNOT.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicCNOT.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicCNOT.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicHIXYZ.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicHIXYZ.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicHIXYZ.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicHIXYZ.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicHIXYZ.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicHIXYZ.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicHIXYZ.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicHIXYZ.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicM.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicM.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicM.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicM.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicM.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicM.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicM.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicM.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithBitFlipCode.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithBitFlipCode.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithBitFlipCode.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithBitFlipCode.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithBitFlipCode.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithBitFlipCode.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithBitFlipCode.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithBitFlipCode.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithPhaseFlipCode.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithPhaseFlipCode.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithPhaseFlipCode.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithPhaseFlipCode.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithPhaseFlipCode.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithPhaseFlipCode.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithPhaseFlipCode.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicMeasureWithPhaseFlipCode.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicRotationsWithPeriod.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicRotationsWithPeriod.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicRotationsWithPeriod.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicRotationsWithPeriod.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicRotationsWithPeriod.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicRotationsWithPeriod.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicRotationsWithPeriod.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicRotationsWithPeriod.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicSTSWAP.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicSTSWAP.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicSTSWAP.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicSTSWAP.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicSTSWAP.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicSTSWAP.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/IntrinsicSTSWAP.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/IntrinsicSTSWAP.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/MeasureAndReuse.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/MeasureAndReuse.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/MeasureAndReuse.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/MeasureAndReuse.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/MeasureAndReuse.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/MeasureAndReuse.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/MeasureAndReuse.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/MeasureAndReuse.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/MeasurementComparison.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/MeasurementComparison.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/MeasurementComparison.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/MeasurementComparison.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/MeasurementComparison.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/MeasurementComparison.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/MeasurementComparison.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/MeasurementComparison.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/NestedBranching.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/NestedBranching.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/NestedBranching.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/NestedBranching.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/NestedBranching.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/NestedBranching.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/NestedBranching.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/NestedBranching.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/RandomBit.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/RandomBit.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/RandomBit.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/RandomBit.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/RandomBit.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/RandomBit.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/RandomBit.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/RandomBit.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/SampleTeleport.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/SampleTeleport.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/SampleTeleport.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/SampleTeleport.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/SampleTeleport.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/SampleTeleport.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/SampleTeleport.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/SampleTeleport.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/ShortcuttingMeasurement.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/ShortcuttingMeasurement.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/ShortcuttingMeasurement.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/ShortcuttingMeasurement.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/ShortcuttingMeasurement.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/ShortcuttingMeasurement.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/ShortcuttingMeasurement.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/ShortcuttingMeasurement.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/Slicing.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/Slicing.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/Slicing.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/Slicing.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/Slicing.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/Slicing.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/Slicing.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/Slicing.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/SuperdenseCoding.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/SuperdenseCoding.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/SuperdenseCoding.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/SuperdenseCoding.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/SuperdenseCoding.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/SuperdenseCoding.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/SuperdenseCoding.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/SuperdenseCoding.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/SwitchHandling.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/SwitchHandling.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/SwitchHandling.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/SwitchHandling.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/SwitchHandling.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/SwitchHandling.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/SwitchHandling.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/SwitchHandling.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/ThreeQubitRepetitionCode.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/ThreeQubitRepetitionCode.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/ThreeQubitRepetitionCode.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/ThreeQubitRepetitionCode.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/ThreeQubitRepetitionCode.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/ThreeQubitRepetitionCode.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/ThreeQubitRepetitionCode.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/ThreeQubitRepetitionCode.out diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/WithinApply.ll b/source/qdk_package/tests-integration/resources/adaptive_ri/output/WithinApply.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/WithinApply.ll rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/WithinApply.ll diff --git a/source/pip/tests-integration/resources/adaptive_ri/output/WithinApply.out b/source/qdk_package/tests-integration/resources/adaptive_ri/output/WithinApply.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_ri/output/WithinApply.out rename to source/qdk_package/tests-integration/resources/adaptive_ri/output/WithinApply.out diff --git a/source/pip/tests-integration/resources/adaptive_rif/input/Doubles.qs b/source/qdk_package/tests-integration/resources/adaptive_rif/input/Doubles.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rif/input/Doubles.qs rename to source/qdk_package/tests-integration/resources/adaptive_rif/input/Doubles.qs diff --git a/source/pip/tests-integration/resources/adaptive_rif/output/Doubles.ll b/source/qdk_package/tests-integration/resources/adaptive_rif/output/Doubles.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rif/output/Doubles.ll rename to source/qdk_package/tests-integration/resources/adaptive_rif/output/Doubles.ll diff --git a/source/pip/tests-integration/resources/adaptive_rif/output/Doubles.out b/source/qdk_package/tests-integration/resources/adaptive_rif/output/Doubles.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rif/output/Doubles.out rename to source/qdk_package/tests-integration/resources/adaptive_rif/output/Doubles.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/ArithmeticOps.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/ArithmeticOps.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/ArithmeticOps.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/ArithmeticOps.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/BernsteinVaziraniNISQ.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/BernsteinVaziraniNISQ.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/BernsteinVaziraniNISQ.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/BernsteinVaziraniNISQ.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/ConstantFolding.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/ConstantFolding.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/ConstantFolding.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/ConstantFolding.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/CopyAndUpdateExpressions.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/CopyAndUpdateExpressions.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/CopyAndUpdateExpressions.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/CopyAndUpdateExpressions.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/DeutschJozsaNISQ.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/DeutschJozsaNISQ.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/DeutschJozsaNISQ.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/DeutschJozsaNISQ.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/Doubles.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/Doubles.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/Doubles.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/Doubles.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/ExpandedTests.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/ExpandedTests.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/ExpandedTests.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/ExpandedTests.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/Functors.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/Functors.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/Functors.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/Functors.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/HiddenShiftNISQ.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/HiddenShiftNISQ.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/HiddenShiftNISQ.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/HiddenShiftNISQ.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/IntegerComparison.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntegerComparison.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/IntegerComparison.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntegerComparison.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicCCNOT.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicCCNOT.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicCCNOT.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicCCNOT.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicCNOT.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicCNOT.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicCNOT.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicCNOT.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicHIXYZ.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicHIXYZ.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicHIXYZ.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicHIXYZ.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicM.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicM.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicM.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicM.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicMeasureWithBitFlipCode.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicMeasureWithBitFlipCode.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicMeasureWithBitFlipCode.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicMeasureWithBitFlipCode.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicMeasureWithPhaseFlipCode.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicMeasureWithPhaseFlipCode.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicMeasureWithPhaseFlipCode.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicMeasureWithPhaseFlipCode.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicRotationsWithPeriod.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicRotationsWithPeriod.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicRotationsWithPeriod.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicRotationsWithPeriod.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicSTSWAP.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicSTSWAP.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/IntrinsicSTSWAP.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/IntrinsicSTSWAP.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/LoopOverArrays.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/LoopOverArrays.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/LoopOverArrays.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/LoopOverArrays.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/MeasureAndReuse.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/MeasureAndReuse.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/MeasureAndReuse.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/MeasureAndReuse.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/MeasurementComparison.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/MeasurementComparison.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/MeasurementComparison.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/MeasurementComparison.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/NestedBranching.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/NestedBranching.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/NestedBranching.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/NestedBranching.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/RUS.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/RUS.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/RUS.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/RUS.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/RandomBit.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/RandomBit.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/RandomBit.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/RandomBit.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/SampleTeleport.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/SampleTeleport.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/SampleTeleport.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/SampleTeleport.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/ShortcuttingMeasurement.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/ShortcuttingMeasurement.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/ShortcuttingMeasurement.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/ShortcuttingMeasurement.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/Slicing.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/Slicing.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/Slicing.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/Slicing.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/SuperdenseCoding.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/SuperdenseCoding.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/SuperdenseCoding.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/SuperdenseCoding.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/SwitchHandling.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/SwitchHandling.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/SwitchHandling.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/SwitchHandling.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/ThreeQubitRepetitionCode.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/ThreeQubitRepetitionCode.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/ThreeQubitRepetitionCode.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/ThreeQubitRepetitionCode.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/input/WithinApply.qs b/source/qdk_package/tests-integration/resources/adaptive_rifla/input/WithinApply.qs similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/input/WithinApply.qs rename to source/qdk_package/tests-integration/resources/adaptive_rifla/input/WithinApply.qs diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/ArithmeticOps.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/ArithmeticOps.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/ArithmeticOps.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/ArithmeticOps.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/ArithmeticOps.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/ArithmeticOps.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/ArithmeticOps.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/ArithmeticOps.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/BernsteinVaziraniNISQ.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/BernsteinVaziraniNISQ.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/BernsteinVaziraniNISQ.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/BernsteinVaziraniNISQ.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/BernsteinVaziraniNISQ.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/BernsteinVaziraniNISQ.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/BernsteinVaziraniNISQ.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/BernsteinVaziraniNISQ.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/ConstantFolding.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/ConstantFolding.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/ConstantFolding.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/ConstantFolding.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/ConstantFolding.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/ConstantFolding.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/ConstantFolding.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/ConstantFolding.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/CopyAndUpdateExpressions.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/CopyAndUpdateExpressions.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/CopyAndUpdateExpressions.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/CopyAndUpdateExpressions.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/CopyAndUpdateExpressions.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/CopyAndUpdateExpressions.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/CopyAndUpdateExpressions.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/CopyAndUpdateExpressions.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/DeutschJozsaNISQ.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/DeutschJozsaNISQ.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/DeutschJozsaNISQ.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/DeutschJozsaNISQ.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/DeutschJozsaNISQ.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/DeutschJozsaNISQ.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/DeutschJozsaNISQ.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/DeutschJozsaNISQ.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/Doubles.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/Doubles.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/Doubles.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/Doubles.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/Doubles.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/Doubles.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/Doubles.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/Doubles.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/ExpandedTests.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/ExpandedTests.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/ExpandedTests.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/ExpandedTests.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/ExpandedTests.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/ExpandedTests.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/ExpandedTests.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/ExpandedTests.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/Functors.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/Functors.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/Functors.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/Functors.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/Functors.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/Functors.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/Functors.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/Functors.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/HiddenShiftNISQ.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/HiddenShiftNISQ.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/HiddenShiftNISQ.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/HiddenShiftNISQ.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/HiddenShiftNISQ.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/HiddenShiftNISQ.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/HiddenShiftNISQ.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/HiddenShiftNISQ.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntegerComparison.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntegerComparison.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntegerComparison.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntegerComparison.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntegerComparison.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntegerComparison.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntegerComparison.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntegerComparison.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicCCNOT.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicCCNOT.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicCCNOT.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicCCNOT.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicCCNOT.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicCCNOT.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicCCNOT.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicCCNOT.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicCNOT.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicCNOT.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicCNOT.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicCNOT.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicCNOT.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicCNOT.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicCNOT.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicCNOT.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicHIXYZ.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicHIXYZ.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicHIXYZ.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicHIXYZ.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicHIXYZ.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicHIXYZ.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicHIXYZ.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicHIXYZ.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicM.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicM.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicM.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicM.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicM.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicM.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicM.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicM.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithBitFlipCode.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithBitFlipCode.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithBitFlipCode.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithBitFlipCode.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithBitFlipCode.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithBitFlipCode.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithBitFlipCode.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithBitFlipCode.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithPhaseFlipCode.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithPhaseFlipCode.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithPhaseFlipCode.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithPhaseFlipCode.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithPhaseFlipCode.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithPhaseFlipCode.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithPhaseFlipCode.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicMeasureWithPhaseFlipCode.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicRotationsWithPeriod.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicRotationsWithPeriod.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicRotationsWithPeriod.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicRotationsWithPeriod.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicRotationsWithPeriod.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicRotationsWithPeriod.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicRotationsWithPeriod.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicRotationsWithPeriod.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicSTSWAP.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicSTSWAP.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicSTSWAP.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicSTSWAP.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicSTSWAP.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicSTSWAP.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/IntrinsicSTSWAP.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/IntrinsicSTSWAP.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/LoopOverArrays.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/LoopOverArrays.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/LoopOverArrays.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/LoopOverArrays.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/LoopOverArrays.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/LoopOverArrays.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/LoopOverArrays.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/LoopOverArrays.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/MeasureAndReuse.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/MeasureAndReuse.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/MeasureAndReuse.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/MeasureAndReuse.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/MeasureAndReuse.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/MeasureAndReuse.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/MeasureAndReuse.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/MeasureAndReuse.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/MeasurementComparison.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/MeasurementComparison.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/MeasurementComparison.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/MeasurementComparison.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/MeasurementComparison.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/MeasurementComparison.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/MeasurementComparison.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/MeasurementComparison.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/NestedBranching.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/NestedBranching.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/NestedBranching.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/NestedBranching.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/NestedBranching.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/NestedBranching.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/NestedBranching.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/NestedBranching.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/RUS.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/RUS.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/RUS.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/RUS.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/RUS.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/RUS.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/RUS.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/RUS.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/RandomBit.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/RandomBit.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/RandomBit.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/RandomBit.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/RandomBit.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/RandomBit.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/RandomBit.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/RandomBit.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/SampleTeleport.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/SampleTeleport.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/SampleTeleport.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/SampleTeleport.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/SampleTeleport.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/SampleTeleport.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/SampleTeleport.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/SampleTeleport.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/ShortcuttingMeasurement.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/ShortcuttingMeasurement.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/ShortcuttingMeasurement.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/ShortcuttingMeasurement.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/ShortcuttingMeasurement.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/ShortcuttingMeasurement.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/ShortcuttingMeasurement.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/ShortcuttingMeasurement.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/Slicing.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/Slicing.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/Slicing.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/Slicing.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/Slicing.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/Slicing.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/Slicing.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/Slicing.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/SuperdenseCoding.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/SuperdenseCoding.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/SuperdenseCoding.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/SuperdenseCoding.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/SuperdenseCoding.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/SuperdenseCoding.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/SuperdenseCoding.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/SuperdenseCoding.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/SwitchHandling.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/SwitchHandling.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/SwitchHandling.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/SwitchHandling.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/SwitchHandling.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/SwitchHandling.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/SwitchHandling.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/SwitchHandling.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/ThreeQubitRepetitionCode.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/ThreeQubitRepetitionCode.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/ThreeQubitRepetitionCode.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/ThreeQubitRepetitionCode.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/ThreeQubitRepetitionCode.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/ThreeQubitRepetitionCode.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/ThreeQubitRepetitionCode.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/ThreeQubitRepetitionCode.out diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/WithinApply.ll b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/WithinApply.ll similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/WithinApply.ll rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/WithinApply.ll diff --git a/source/pip/tests-integration/resources/adaptive_rifla/output/WithinApply.out b/source/qdk_package/tests-integration/resources/adaptive_rifla/output/WithinApply.out similarity index 100% rename from source/pip/tests-integration/resources/adaptive_rifla/output/WithinApply.out rename to source/qdk_package/tests-integration/resources/adaptive_rifla/output/WithinApply.out diff --git a/source/pip/tests-integration/test_adaptive_ri_qir.py b/source/qdk_package/tests-integration/test_adaptive_ri_qir.py similarity index 98% rename from source/pip/tests-integration/test_adaptive_ri_qir.py rename to source/qdk_package/tests-integration/test_adaptive_ri_qir.py index 3fc2c9f117..2970bb4dda 100644 --- a/source/pip/tests-integration/test_adaptive_ri_qir.py +++ b/source/qdk_package/tests-integration/test_adaptive_ri_qir.py @@ -4,7 +4,7 @@ import pytest -from qsharp import TargetProfile +from qdk import TargetProfile from utils import ( assert_strings_equal_ignore_line_endings, compile_qsharp, diff --git a/source/pip/tests-integration/test_adaptive_rif_qir.py b/source/qdk_package/tests-integration/test_adaptive_rif_qir.py similarity index 98% rename from source/pip/tests-integration/test_adaptive_rif_qir.py rename to source/qdk_package/tests-integration/test_adaptive_rif_qir.py index 87891f549a..61a0ce0996 100644 --- a/source/pip/tests-integration/test_adaptive_rif_qir.py +++ b/source/qdk_package/tests-integration/test_adaptive_rif_qir.py @@ -4,7 +4,7 @@ import pytest -from qsharp import TargetProfile +from qdk import TargetProfile from utils import ( assert_strings_equal_ignore_line_endings, compile_qsharp, diff --git a/source/pip/tests-integration/test_adaptive_rifla_qir.py b/source/qdk_package/tests-integration/test_adaptive_rifla_qir.py similarity index 98% rename from source/pip/tests-integration/test_adaptive_rifla_qir.py rename to source/qdk_package/tests-integration/test_adaptive_rifla_qir.py index ae07e4af44..f9eabc9cf7 100644 --- a/source/pip/tests-integration/test_adaptive_rifla_qir.py +++ b/source/qdk_package/tests-integration/test_adaptive_rifla_qir.py @@ -4,7 +4,7 @@ import pytest -from qsharp import TargetProfile +from qdk import TargetProfile from utils import ( assert_strings_equal_ignore_line_endings, compile_qsharp, diff --git a/source/pip/tests-integration/test_base_qir.py b/source/qdk_package/tests-integration/test_base_qir.py similarity index 99% rename from source/pip/tests-integration/test_base_qir.py rename to source/qdk_package/tests-integration/test_base_qir.py index 05646cc568..9d773a33db 100644 --- a/source/pip/tests-integration/test_base_qir.py +++ b/source/qdk_package/tests-integration/test_base_qir.py @@ -3,8 +3,7 @@ import pytest -import qsharp - +import qdk as qsharp try: from pyqir import ( Call, diff --git a/source/pip/tests-integration/test_requirements.txt b/source/qdk_package/tests-integration/test_requirements.txt similarity index 100% rename from source/pip/tests-integration/test_requirements.txt rename to source/qdk_package/tests-integration/test_requirements.txt diff --git a/source/pip/tests-integration/utils.py b/source/qdk_package/tests-integration/utils.py similarity index 98% rename from source/pip/tests-integration/utils.py rename to source/qdk_package/tests-integration/utils.py index 796e8db57b..9206aebdbd 100644 --- a/source/pip/tests-integration/utils.py +++ b/source/qdk_package/tests-integration/utils.py @@ -12,7 +12,7 @@ import os -from qsharp._native import ( +from qdk._native import ( Interpreter, TargetProfile, QSharpError, @@ -121,7 +121,7 @@ def get_interpreter( manifest_descriptor = None language_features = None - from qsharp._fs import read_file, list_directory + from qdk._fs import read_file, list_directory interpreter = Interpreter( target_profile, diff --git a/source/pip/tests/.gitignore b/source/qdk_package/tests/.gitignore similarity index 100% rename from source/pip/tests/.gitignore rename to source/qdk_package/tests/.gitignore diff --git a/source/pip/tests/CliffordCalls.qs b/source/qdk_package/tests/CliffordCalls.qs similarity index 100% rename from source/pip/tests/CliffordCalls.qs rename to source/qdk_package/tests/CliffordCalls.qs diff --git a/source/pip/tests/CliffordIsing.qs b/source/qdk_package/tests/CliffordIsing.qs similarity index 100% rename from source/pip/tests/CliffordIsing.qs rename to source/qdk_package/tests/CliffordIsing.qs diff --git a/source/pip/tests/applications/__init__.py b/source/qdk_package/tests/applications/__init__.py similarity index 100% rename from source/pip/tests/applications/__init__.py rename to source/qdk_package/tests/applications/__init__.py diff --git a/source/pip/tests/applications/magnets/__init__.py b/source/qdk_package/tests/applications/magnets/__init__.py similarity index 100% rename from source/pip/tests/applications/magnets/__init__.py rename to source/qdk_package/tests/applications/magnets/__init__.py diff --git a/source/pip/tests/applications/magnets/test_complete.py b/source/qdk_package/tests/applications/magnets/test_complete.py similarity index 99% rename from source/pip/tests/applications/magnets/test_complete.py rename to source/qdk_package/tests/applications/magnets/test_complete.py index 50a50ac1ef..aafd6f3d3a 100644 --- a/source/pip/tests/applications/magnets/test_complete.py +++ b/source/qdk_package/tests/applications/magnets/test_complete.py @@ -7,7 +7,7 @@ cirq = pytest.importorskip("cirq") -from qsharp.applications.magnets import ( +from qdk.applications.magnets import ( CompleteBipartiteGraph, CompleteGraph, Hypergraph, diff --git a/source/pip/tests/applications/magnets/test_hypergraph.py b/source/qdk_package/tests/applications/magnets/test_hypergraph.py old mode 100755 new mode 100644 similarity index 99% rename from source/pip/tests/applications/magnets/test_hypergraph.py rename to source/qdk_package/tests/applications/magnets/test_hypergraph.py index 0c633a1d84..297f471be7 --- a/source/pip/tests/applications/magnets/test_hypergraph.py +++ b/source/qdk_package/tests/applications/magnets/test_hypergraph.py @@ -8,7 +8,7 @@ cirq = pytest.importorskip("cirq") -from qsharp.applications.magnets import ( +from qdk.applications.magnets import ( Hyperedge, Hypergraph, HypergraphEdgeColoring, diff --git a/source/pip/tests/applications/magnets/test_lattice1d.py b/source/qdk_package/tests/applications/magnets/test_lattice1d.py similarity index 99% rename from source/pip/tests/applications/magnets/test_lattice1d.py rename to source/qdk_package/tests/applications/magnets/test_lattice1d.py index f9b06086be..b122ef0f59 100644 --- a/source/pip/tests/applications/magnets/test_lattice1d.py +++ b/source/qdk_package/tests/applications/magnets/test_lattice1d.py @@ -7,7 +7,7 @@ cirq = pytest.importorskip("cirq") -from qsharp.applications.magnets import ( +from qdk.applications.magnets import ( Chain1D, Hypergraph, HypergraphEdgeColoring, diff --git a/source/pip/tests/applications/magnets/test_lattice2d.py b/source/qdk_package/tests/applications/magnets/test_lattice2d.py similarity index 99% rename from source/pip/tests/applications/magnets/test_lattice2d.py rename to source/qdk_package/tests/applications/magnets/test_lattice2d.py index bedb7874b6..8c53b3c94e 100644 --- a/source/pip/tests/applications/magnets/test_lattice2d.py +++ b/source/qdk_package/tests/applications/magnets/test_lattice2d.py @@ -7,7 +7,7 @@ cirq = pytest.importorskip("cirq") -from qsharp.applications.magnets import ( +from qdk.applications.magnets import ( Hypergraph, HypergraphEdgeColoring, Patch2D, diff --git a/source/pip/tests/applications/magnets/test_model.py b/source/qdk_package/tests/applications/magnets/test_model.py old mode 100755 new mode 100644 similarity index 99% rename from source/pip/tests/applications/magnets/test_model.py rename to source/qdk_package/tests/applications/magnets/test_model.py index c525a80719..52a684f3ec --- a/source/pip/tests/applications/magnets/test_model.py +++ b/source/qdk_package/tests/applications/magnets/test_model.py @@ -11,7 +11,7 @@ cirq = pytest.importorskip("cirq") -from qsharp.applications.magnets import ( +from qdk.applications.magnets import ( HeisenbergModel, Hyperedge, Hypergraph, diff --git a/source/pip/tests/applications/magnets/test_pauli.py b/source/qdk_package/tests/applications/magnets/test_pauli.py similarity index 98% rename from source/pip/tests/applications/magnets/test_pauli.py rename to source/qdk_package/tests/applications/magnets/test_pauli.py index bb4f7ab8dd..306e6b4597 100644 --- a/source/pip/tests/applications/magnets/test_pauli.py +++ b/source/qdk_package/tests/applications/magnets/test_pauli.py @@ -7,7 +7,7 @@ cirq = pytest.importorskip("cirq") -from qsharp.applications.magnets import Pauli, PauliString, PauliX, PauliY, PauliZ +from qdk.applications.magnets import Pauli, PauliString, PauliX, PauliY, PauliZ def test_pauli_init_from_int_and_string(): diff --git a/source/pip/tests/applications/magnets/test_trotter.py b/source/qdk_package/tests/applications/magnets/test_trotter.py similarity index 99% rename from source/pip/tests/applications/magnets/test_trotter.py rename to source/qdk_package/tests/applications/magnets/test_trotter.py index 77d508ab7e..f0e2f435db 100644 --- a/source/pip/tests/applications/magnets/test_trotter.py +++ b/source/qdk_package/tests/applications/magnets/test_trotter.py @@ -7,7 +7,7 @@ cirq = pytest.importorskip("cirq") -from qsharp.applications.magnets import ( +from qdk.applications.magnets import ( Hyperedge, Hypergraph, Model, diff --git a/source/pip/tests/circuit.qsc b/source/qdk_package/tests/circuit.qsc similarity index 100% rename from source/pip/tests/circuit.qsc rename to source/qdk_package/tests/circuit.qsc diff --git a/source/qdk_package/tests/conftest.py b/source/qdk_package/tests/conftest.py index 4013081ef4..b83f73269f 100644 --- a/source/qdk_package/tests/conftest.py +++ b/source/qdk_package/tests/conftest.py @@ -1,17 +1,40 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import sys -from pathlib import Path +# Tests rely on the qdk wheel being installed in the venv (with the compiled +# _native extension). Do NOT add the source tree to sys.path here – that +# would shadow the installed package with the local source directory, which +# does not contain the compiled extension module. -# Add local package root so 'qdk' can be imported without installation and add tests dir so 'mocks' is importable. -_root = Path(__file__).resolve().parent -_pkg_root = _root.parent -for p in (_pkg_root, _root): - if str(p) not in sys.path: - sys.path.insert(0, str(p)) +# --------------------------------------------------------------------------- +# Many test files use ``import qdk as qsharp`` and then access symbols like +# ``qsharp.eval()``, ``qsharp.run()``, etc. Those symbols were part of the +# old *qsharp* public API but are intentionally NOT exported from ``qdk`` +# (whose public API must stay unchanged). +# +# Rather than rewriting hundreds of call-sites, we monkey-patch the extra +# symbols onto the ``qdk`` module here so the test alias keeps working. +# This is test infrastructure only – it does not affect the public package. +# --------------------------------------------------------------------------- +import qdk +from qdk._interpreter import ( # noqa: E402 + eval, + run, + compile, + circuit, + estimate, + logical_counts, + dump_operation, +) +from qdk._native import QSharpError, CircuitGenerationMethod, estimate_custom # type: ignore # noqa: E402 -# Ensure a qsharp stub (if real package absent) via centralized mocks helper. -import mocks - -mocks.mock_qsharp() +qdk.eval = eval +qdk.run = run +qdk.compile = compile +qdk.circuit = circuit +qdk.estimate = estimate +qdk.logical_counts = logical_counts +qdk.QSharpError = QSharpError +qdk.CircuitGenerationMethod = CircuitGenerationMethod +qdk.estimate_custom = estimate_custom +qdk.dump_operation = dump_operation diff --git a/source/pip/tests/csv_dir_test/test_noise_intrinsic.csv b/source/qdk_package/tests/csv_dir_test/test_noise_intrinsic.csv similarity index 100% rename from source/pip/tests/csv_dir_test/test_noise_intrinsic.csv rename to source/qdk_package/tests/csv_dir_test/test_noise_intrinsic.csv diff --git a/source/qdk_package/tests/mocks.py b/source/qdk_package/tests/mocks.py deleted file mode 100644 index 430edaa761..0000000000 --- a/source/qdk_package/tests/mocks.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -"""Centralized mock helpers for tests. - -Provides lightweight stand-ins for optional (and required during tests) dependencies. - -Functions return a list of module names they created so callers can later clean them -up using cleanup_modules(). This keeps test intent explicit. -""" - -import sys -import types -from typing import List - - -def _not_impl(*_a, **_k): - raise NotImplementedError("qsharp stub: real 'qsharp' package not installed") - - -def mock_qsharp() -> List[str]: - """Ensure a minimal 'qsharp' module exists. - - In real usage the qsharp package provides a compiled extension. Tests only - need the attribute surface that qdk re-exports (run/estimate presently used - for sanity checks). If the real package is installed this is a no-op. - """ - created: List[str] = [] - if "qsharp" not in sys.modules: - stub = types.ModuleType("qsharp") - - stub.run = _not_impl - stub.estimate = _not_impl - # Provide utility symbols expected to re-export at root - stub.code = object() - stub.set_quantum_seed = _not_impl - stub.set_classical_seed = _not_impl - stub.dump_machine = _not_impl - stub.init = _not_impl - - class _T: # placeholder types - pass - - stub.Result = _T - stub.TargetProfile = _T - stub.StateDump = _T - stub.ShotResult = _T - stub.PauliNoise = _T - stub.DepolarizingNoise = _T - stub.BitFlipNoise = _T - stub.PhaseFlipNoise = _T - stub.__all__ = [ - "run", - "estimate", - "code", - "set_quantum_seed", - "set_classical_seed", - "dump_machine", - "init", - "Result", - "TargetProfile", - "StateDump", - "ShotResult", - "PauliNoise", - "DepolarizingNoise", - "BitFlipNoise", - "PhaseFlipNoise", - "estimator", - "openqasm", - "utils", - ] - # Minimal submodules to back lifted shims - est = types.ModuleType("qsharp.estimator") - est.__doc__ = "mock estimator" - sys.modules["qsharp.estimator"] = est - stub.estimator = est - oq = types.ModuleType("qsharp.openqasm") - oq.__doc__ = "mock openqasm" - sys.modules["qsharp.openqasm"] = oq - stub.openqasm = oq - utils_mod = types.ModuleType("qsharp.utils") - utils_mod.dump_operation = _not_impl - sys.modules["qsharp.utils"] = utils_mod - stub.utils = utils_mod - - sys.modules["qsharp"] = stub - # Telemetry events package with on_qdk_import function expected by qdk import - telemetry_pkg = types.ModuleType("qsharp.telemetry_events") - - def on_qdk_import(): - return None - - telemetry_pkg.on_qdk_import = on_qdk_import - sys.modules["qsharp.telemetry_events"] = telemetry_pkg - # Interop namespace for qiskit shim expectations - interop = types.ModuleType("qsharp.interop") - sys.modules["qsharp.interop"] = interop - interop_qk = types.ModuleType("qsharp.interop.qiskit") - interop_qk.__doc__ = "mock qsharp interop qiskit" - sys.modules["qsharp.interop.qiskit"] = interop_qk - - created.extend( - [ - "qsharp", - "qsharp.estimator", - "qsharp.openqasm", - "qsharp.utils", - "qsharp.telemetry_events", - "qsharp.interop", - "qsharp.interop.qiskit", - ] - ) - return created - - -def mock_widgets() -> List[str]: - created: List[str] = [] - if "qsharp_widgets" not in sys.modules: - mod = types.ModuleType("qsharp_widgets") - sys.modules["qsharp_widgets"] = mod - created.append("qsharp_widgets") - return created - - -def mock_azure() -> List[str]: - created: List[str] = [] - if "azure" not in sys.modules: - sys.modules["azure"] = types.ModuleType("azure") - created.append("azure") - if "azure.quantum" not in sys.modules: - aq = types.ModuleType("azure.quantum") - # Minimal submodules expected by qdk.azure shim - tgt = types.ModuleType("azure.quantum.target") - argt = types.ModuleType("azure.quantum.argument_types") - job = types.ModuleType("azure.quantum.job") - # Register in sys.modules first - sys.modules["azure.quantum.target"] = tgt - sys.modules["azure.quantum.argument_types"] = argt - sys.modules["azure.quantum.job"] = job - # Attach to parent for attribute access - aq.target = tgt - aq.argument_types = argt - aq.job = job - sys.modules["azure.quantum"] = aq - created.extend( - [ - "azure.quantum", - "azure.quantum.target", - "azure.quantum.argument_types", - "azure.quantum.job", - ] - ) - return created - - -def mock_qiskit() -> List[str]: - created: List[str] = [] - if "qiskit" not in sys.modules: - qk = types.ModuleType("qiskit") - qk.transpile = _not_impl - sys.modules["qiskit"] = qk - created.append("qiskit") - return created - - -def mock_cirq() -> List[str]: - created: List[str] = [] - if "cirq" not in sys.modules: - cq = types.ModuleType("cirq") - sys.modules["cirq"] = cq - created.append("cirq") - if "qsharp.interop.cirq" not in sys.modules: - interop_cirq = types.ModuleType("qsharp.interop.cirq") - interop_cirq.__doc__ = "mock qsharp interop cirq" - sys.modules["qsharp.interop.cirq"] = interop_cirq - interop = sys.modules.get("qsharp.interop") - if interop is not None: - interop.cirq = interop_cirq - created.append("qsharp.interop.cirq") - return created - - -def cleanup_modules(created: List[str]) -> None: - """Remove synthetic modules created during a test if still present.""" - for name in created: - sys.modules.pop(name, None) diff --git a/source/pip/tests/qre/__init__.py b/source/qdk_package/tests/qre/__init__.py similarity index 100% rename from source/pip/tests/qre/__init__.py rename to source/qdk_package/tests/qre/__init__.py diff --git a/source/pip/tests/qre/conftest.py b/source/qdk_package/tests/qre/conftest.py similarity index 91% rename from source/pip/tests/qre/conftest.py rename to source/qdk_package/tests/qre/conftest.py index c779e6ff31..8e0b3394ae 100644 --- a/source/pip/tests/qre/conftest.py +++ b/source/qdk_package/tests/qre/conftest.py @@ -4,15 +4,15 @@ from dataclasses import KW_ONLY, dataclass, field from typing import Generator -from qsharp.qre import ( +from qdk.qre import ( ISA, LOGICAL, ISARequirements, ISATransform, constraint, ) -from qsharp.qre._architecture import ISAContext -from qsharp.qre.instruction_ids import LATTICE_SURGERY, T +from qdk.qre._architecture import ISAContext +from qdk.qre.instruction_ids import LATTICE_SURGERY, T # NOTE These classes will be generalized as part of the QRE API in the following diff --git a/source/pip/tests/qre/test_application.py b/source/qdk_package/tests/qre/test_application.py similarity index 95% rename from source/pip/tests/qre/test_application.py rename to source/qdk_package/tests/qre/test_application.py index f49db0921b..f3045aad37 100644 --- a/source/pip/tests/qre/test_application.py +++ b/source/qdk_package/tests/qre/test_application.py @@ -7,9 +7,9 @@ from dataclasses import dataclass, field -import qsharp +import qdk as qsharp -from qsharp.qre import ( +from qdk.qre import ( Application, ISA, LOGICAL, @@ -19,11 +19,11 @@ Trace, linear_function, ) -from qsharp.qre._qre import _ProvenanceGraph -from qsharp.qre._enumeration import _enumerate_instances -from qsharp.qre.application import QSharpApplication -from qsharp.qre.instruction_ids import CCX, LATTICE_SURGERY, T, RZ -from qsharp.qre.property_keys import ( +from qdk.qre._qre import _ProvenanceGraph +from qdk.qre._enumeration import _enumerate_instances +from qdk.qre.application import QSharpApplication +from qdk.qre.instruction_ids import CCX, LATTICE_SURGERY, T, RZ +from qdk.qre.property_keys import ( ALGORITHM_COMPUTE_QUBITS, ALGORITHM_MEMORY_QUBITS, LOGICAL_COMPUTE_QUBITS, diff --git a/source/pip/tests/qre/test_cirq_interop.py b/source/qdk_package/tests/qre/test_cirq_interop.py similarity index 97% rename from source/pip/tests/qre/test_cirq_interop.py rename to source/qdk_package/tests/qre/test_cirq_interop.py index 3e01eb8b12..95bcf8fe04 100644 --- a/source/pip/tests/qre/test_cirq_interop.py +++ b/source/qdk_package/tests/qre/test_cirq_interop.py @@ -5,10 +5,10 @@ cirq = pytest.importorskip("cirq") -from qsharp.qre import PSSPC -from qsharp.qre.application import CirqApplication -from qsharp.qre.interop import trace_from_cirq -from qsharp.qre.interop._cirq import ( +from qdk.qre import PSSPC +from qdk.qre.application import CirqApplication +from qdk.qre.interop import trace_from_cirq +from qdk.qre.interop._cirq import ( TypedQubit, QubitType, read_from_memory, diff --git a/source/pip/tests/qre/test_enumeration.py b/source/qdk_package/tests/qre/test_enumeration.py similarity index 94% rename from source/pip/tests/qre/test_enumeration.py rename to source/qdk_package/tests/qre/test_enumeration.py index 371ca5126f..d8c987f90c 100644 --- a/source/pip/tests/qre/test_enumeration.py +++ b/source/qdk_package/tests/qre/test_enumeration.py @@ -7,10 +7,10 @@ import pytest -from qsharp.qre import LOGICAL -from qsharp.qre.models import SurfaceCode, GateBased, RoundBasedFactory -from qsharp.qre.instruction_ids import LATTICE_SURGERY, T -from qsharp.qre._isa_enumeration import ( +from qdk.qre import LOGICAL +from qdk.qre.models import SurfaceCode, GateBased, RoundBasedFactory +from qdk.qre.instruction_ids import LATTICE_SURGERY, T +from qdk.qre._isa_enumeration import ( ISARefNode, _ComponentQuery, _ProductNode, @@ -22,7 +22,7 @@ def test_enumerate_instances(): """Test enumeration of SurfaceCode instances with default and custom domains.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances instances = list(_enumerate_instances(SurfaceCode)) @@ -47,7 +47,7 @@ def test_enumerate_instances(): def test_enumerate_instances_bool(): """Test that boolean dataclass fields enumerate both True and False.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances @dataclass class BoolConfig: @@ -62,7 +62,7 @@ class BoolConfig: def test_enumerate_instances_enum(): """Test that Enum dataclass fields enumerate all members.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances class Color(Enum): RED = 1 @@ -83,7 +83,7 @@ class EnumConfig: def test_enumerate_instances_failure(): """Test that a field with no domain and no default raises ValueError.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances @dataclass class InvalidConfig: @@ -97,7 +97,7 @@ class InvalidConfig: def test_enumerate_instances_single(): """Test enumeration of a dataclass with a single non-kw-only field.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances @dataclass class SingleConfig: @@ -110,7 +110,7 @@ class SingleConfig: def test_enumerate_instances_literal(): """Test that Literal-typed fields enumerate their allowed values.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances from typing import Literal @@ -127,7 +127,7 @@ class LiteralConfig: def test_enumerate_instances_nested(): """Test enumeration of nested dataclass fields.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances @dataclass class InnerConfig: @@ -147,7 +147,7 @@ class OuterConfig: def test_enumerate_instances_union(): """Test enumeration of union-typed dataclass fields.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances @dataclass class OptionA: @@ -174,7 +174,7 @@ class UnionConfig: def test_enumerate_instances_nested_with_constraints(): """Test constraining nested dataclass fields via a dict.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances @dataclass class InnerConfig: @@ -194,7 +194,7 @@ class OuterConfig: def test_enumerate_instances_union_single_type(): """Test restricting a union field to a single member type.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances @dataclass class OptionA: @@ -227,7 +227,7 @@ class UnionConfig: def test_enumerate_instances_union_list_of_types(): """Test restricting a union field to a subset of member types.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances @dataclass class OptionA: @@ -257,7 +257,7 @@ class UnionConfig: def test_enumerate_instances_union_constraint_dict(): """Test constraining union field members via a type-to-kwargs dict.""" - from qsharp.qre._enumeration import _enumerate_instances + from qdk.qre._enumeration import _enumerate_instances @dataclass class OptionA: diff --git a/source/pip/tests/qre/test_estimation.py b/source/qdk_package/tests/qre/test_estimation.py similarity index 94% rename from source/pip/tests/qre/test_estimation.py rename to source/qdk_package/tests/qre/test_estimation.py index ab61887e5f..7026d90f9b 100644 --- a/source/pip/tests/qre/test_estimation.py +++ b/source/qdk_package/tests/qre/test_estimation.py @@ -7,14 +7,14 @@ cirq = pytest.importorskip("cirq") -from qsharp.estimator import LogicalCounts -from qsharp.qre import ( +from qdk.estimator import LogicalCounts +from qdk.qre import ( PSSPC, LatticeSurgery, estimate, ) -from qsharp.qre.application import QSharpApplication -from qsharp.qre.models import ( +from qdk.qre.application import QSharpApplication +from qdk.qre.models import ( SurfaceCode, GateBased, RoundBasedFactory, diff --git a/source/pip/tests/qre/test_estimation_table.py b/source/qdk_package/tests/qre/test_estimation_table.py similarity index 96% rename from source/pip/tests/qre/test_estimation_table.py rename to source/qdk_package/tests/qre/test_estimation_table.py index 3cb09451ed..abce2a79ac 100644 --- a/source/pip/tests/qre/test_estimation_table.py +++ b/source/qdk_package/tests/qre/test_estimation_table.py @@ -7,20 +7,20 @@ import pandas as pd -from qsharp.qre import ( +from qdk.qre import ( PSSPC, LatticeSurgery, estimate, ) -from qsharp.qre.application import QSharpApplication -from qsharp.qre.models import SurfaceCode, GateBased -from qsharp.qre._estimation import ( +from qdk.qre.application import QSharpApplication +from qdk.qre.models import SurfaceCode, GateBased +from qdk.qre._estimation import ( EstimationTable, EstimationTableEntry, ) -from qsharp.qre._instruction import InstructionSource -from qsharp.qre.instruction_ids import LATTICE_SURGERY -from qsharp.qre.property_keys import DISTANCE, NUM_TS_PER_ROTATION +from qdk.qre._instruction import InstructionSource +from qdk.qre.instruction_ids import LATTICE_SURGERY +from qdk.qre.property_keys import DISTANCE, NUM_TS_PER_ROTATION from .conftest import ExampleFactory diff --git a/source/pip/tests/qre/test_interop.py b/source/qdk_package/tests/qre/test_interop.py similarity index 96% rename from source/pip/tests/qre/test_interop.py rename to source/qdk_package/tests/qre/test_interop.py index 99416d6bb7..1d9bd55a8b 100644 --- a/source/pip/tests/qre/test_interop.py +++ b/source/qdk_package/tests/qre/test_interop.py @@ -7,9 +7,9 @@ cirq = pytest.importorskip("cirq") -import qsharp -from qsharp.qre.application import QSharpApplication, QIRApplication -from qsharp.qre.interop import trace_from_qir +import qdk as qsharp +from qdk.qre.application import QSharpApplication, QIRApplication +from qdk.qre.interop import trace_from_qir def _ll_files(): @@ -50,8 +50,8 @@ def test_trace_from_qir_handles_all_instruction_ids(): """ import pyqir import pyqir.qis as qis - from qsharp._native import QirInstructionId - from qsharp.qre.interop._qir import _GATE_MAP, _MEAS_MAP, _SKIP + from qdk._native import QirInstructionId + from qdk.qre.interop._qir import _GATE_MAP, _MEAS_MAP, _SKIP # -- Completeness check: every QirInstructionId must be covered -------- handled_ids = ( @@ -198,7 +198,7 @@ def declare(name, param_types): def test_rotation_buckets(): """Test that rotation bucketization preserves total count and depth.""" - from qsharp.qre.interop._qsharp import _bucketize_rotation_counts + from qdk.qre.interop._qsharp import _bucketize_rotation_counts r_count = 15066 r_depth = 14756 diff --git a/source/pip/tests/qre/test_isa.py b/source/qdk_package/tests/qre/test_isa.py similarity index 94% rename from source/pip/tests/qre/test_isa.py rename to source/qdk_package/tests/qre/test_isa.py index e8fda19f29..7edff0dbe2 100644 --- a/source/pip/tests/qre/test_isa.py +++ b/source/qdk_package/tests/qre/test_isa.py @@ -3,7 +3,7 @@ import pytest -from qsharp.qre import ( +from qdk.qre import ( LOGICAL, ISARequirements, constraint, @@ -11,11 +11,11 @@ property_name, property_name_to_key, ) -from qsharp.qre._qre import _ProvenanceGraph -from qsharp.qre.models import SurfaceCode, GateBased -from qsharp.qre._architecture import _make_instruction -from qsharp.qre.instruction_ids import CCX, CCZ, LATTICE_SURGERY, T -from qsharp.qre.property_keys import DISTANCE +from qdk.qre._qre import _ProvenanceGraph +from qdk.qre.models import SurfaceCode, GateBased +from qdk.qre._architecture import _make_instruction +from qdk.qre.instruction_ids import CCX, CCZ, LATTICE_SURGERY, T +from qdk.qre.property_keys import DISTANCE def test_isa(): @@ -146,7 +146,7 @@ def test_property_names(): def test_block_linear_function(): """Test block_linear_function creation and behavior.""" - from qsharp.qre._qre import block_linear_function + from qdk.qre._qre import block_linear_function # Test int version with offset int_fn = block_linear_function(block_size=4, slope=2, offset=1) @@ -183,7 +183,7 @@ def test_block_linear_function(): def test_generic_function(): """Test generic_function wrapping for int and float return types.""" - from qsharp.qre._qre import _IntFunction, _FloatFunction + from qdk.qre._qre import _IntFunction, _FloatFunction def time(x: int) -> int: return x * x diff --git a/source/pip/tests/qre/test_models.py b/source/qdk_package/tests/qre/test_models.py similarity index 99% rename from source/pip/tests/qre/test_models.py rename to source/qdk_package/tests/qre/test_models.py index 46b236afb0..46f23b88be 100644 --- a/source/pip/tests/qre/test_models.py +++ b/source/qdk_package/tests/qre/test_models.py @@ -1,8 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from qsharp.qre import LOGICAL, PHYSICAL -from qsharp.qre.instruction_ids import ( +from qdk.qre import LOGICAL, PHYSICAL +from qdk.qre.instruction_ids import ( T, CCZ, CCX, @@ -26,7 +26,7 @@ SQRT_SQRT_Z, SQRT_SQRT_Z_DAG, ) -from qsharp.qre.models import ( +from qdk.qre.models import ( GateBased, Majorana, RoundBasedFactory, @@ -36,7 +36,7 @@ ThreeAux, TwoDimensionalYokedSurfaceCode, ) -from qsharp.qre.property_keys import DISTANCE +from qdk.qre.property_keys import DISTANCE # --------------------------------------------------------------------------- @@ -580,7 +580,7 @@ def test_table1_1e3_clifford_yields_6_isas(self): def test_table2_scenario_no_ccz(self): """Table 2 scenario: T error ~10x higher than Clifford, no CCZ.""" - from qsharp.qre._qre import _ProvenanceGraph + from qdk.qre._qre import _ProvenanceGraph arch = GateBased(gate_time=50, measurement_time=100) ctx = arch.context() @@ -608,7 +608,7 @@ def test_table2_scenario_no_ccz(self): def test_no_yield_when_error_too_high(self): """If T error > 10x Clifford, no entries match.""" - from qsharp.qre._qre import _ProvenanceGraph + from qdk.qre._qre import _ProvenanceGraph arch = GateBased(gate_time=50, measurement_time=100) ctx = arch.context() @@ -756,14 +756,14 @@ def test_modification_count_matches_factory_output(self): def test_no_family_present_passes_through(self): """If no family member is present, ISA passes through unchanged.""" - from qsharp.qre._qre import _ProvenanceGraph + from qdk.qre._qre import _ProvenanceGraph arch = GateBased(gate_time=50, measurement_time=100) ctx = arch.context() modifier = MagicUpToClifford() # ISA with only a LATTICE_SURGERY instruction (no T or CCZ family) - from qsharp.qre import linear_function + from qdk.qre import linear_function graph = _ProvenanceGraph() isa_input = graph.make_isa( diff --git a/source/qdk_package/tests/reexports/__init__.py b/source/qdk_package/tests/reexports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/qdk_package/tests/reexports/conftest.py b/source/qdk_package/tests/reexports/conftest.py new file mode 100644 index 0000000000..16a794a909 --- /dev/null +++ b/source/qdk_package/tests/reexports/conftest.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import sys +from pathlib import Path + +# Make the sibling ``mocks`` module importable without installation. +_dir = Path(__file__).resolve().parent +if str(_dir) not in sys.path: + sys.path.insert(0, str(_dir)) diff --git a/source/qdk_package/tests/reexports/mocks.py b/source/qdk_package/tests/reexports/mocks.py new file mode 100644 index 0000000000..3c4a2698d1 --- /dev/null +++ b/source/qdk_package/tests/reexports/mocks.py @@ -0,0 +1,73 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Centralized mock helpers for tests. + +Provides lightweight stand-ins for optional dependencies. + +Functions return a list of module names they created so callers can later clean them +up using cleanup_modules(). This keeps test intent explicit. +""" + +import sys +import types +from typing import List + + +def _not_impl(*_a, **_k): + raise NotImplementedError("stub: dependency not installed") + + +def mock_widgets() -> List[str]: + created: List[str] = [] + if "qsharp_widgets" not in sys.modules: + mod = types.ModuleType("qsharp_widgets") + sys.modules["qsharp_widgets"] = mod + created.append("qsharp_widgets") + return created + + +def mock_azure() -> List[str]: + created: List[str] = [] + if "azure" not in sys.modules: + sys.modules["azure"] = types.ModuleType("azure") + created.append("azure") + if "azure.quantum" not in sys.modules: + aq = types.ModuleType("azure.quantum") + # Minimal submodules expected by qdk.azure shim + tgt = types.ModuleType("azure.quantum.target") + argt = types.ModuleType("azure.quantum.argument_types") + job = types.ModuleType("azure.quantum.job") + # Register in sys.modules first + sys.modules["azure.quantum.target"] = tgt + sys.modules["azure.quantum.argument_types"] = argt + sys.modules["azure.quantum.job"] = job + # Attach to parent for attribute access + aq.target = tgt + aq.argument_types = argt + aq.job = job + sys.modules["azure.quantum"] = aq + created.extend( + [ + "azure.quantum", + "azure.quantum.target", + "azure.quantum.argument_types", + "azure.quantum.job", + ] + ) + return created + + +def cleanup_modules(created: List[str]) -> None: + """Remove synthetic modules created during a test if still present. + + Also removes qdk shim modules that depend on them so that subsequent + tests re-trigger the import machinery instead of getting cached results. + """ + for name in created: + sys.modules.pop(name, None) + + # The qdk re-export shims cache themselves in sys.modules after import. + # Remove them so later tests that check missing-dep behavior work correctly. + sys.modules.pop("qdk.widgets", None) + sys.modules.pop("qdk.azure", None) diff --git a/source/qdk_package/tests/test_extras.py b/source/qdk_package/tests/reexports/test_extras.py similarity index 51% rename from source/qdk_package/tests/test_extras.py rename to source/qdk_package/tests/reexports/test_extras.py index 80b574805e..922a955525 100644 --- a/source/qdk_package/tests/test_extras.py +++ b/source/qdk_package/tests/reexports/test_extras.py @@ -1,19 +1,20 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import pytest, importlib +"""Tests for the re-export shims that wrap optional third-party packages. -from mocks import ( - mock_widgets, - mock_azure, - mock_qiskit, - mock_cirq, - cleanup_modules, -) +Only ``qdk.widgets`` (wraps ``qsharp_widgets``) and ``qdk.azure`` (wraps +``azure.quantum``) are re-export shims. We mock the upstream packages and +verify that the shims surface the expected attributes. +""" +import importlib +import pytest -# Standard contract description for each extra we test. -EXTRAS = { +from mocks import mock_widgets, mock_azure, cleanup_modules + + +MOCK_EXTRAS = { "widgets": { "mock": mock_widgets, "module": "qdk.widgets", @@ -26,21 +27,11 @@ hasattr(mod, name) for name in ("target", "argument_types", "job") ), }, - "qiskit": { - "mock": mock_qiskit, - "module": "qdk.qiskit", - "post_assert": lambda mod: hasattr(mod, "__doc__"), - }, - "cirq": { - "mock": mock_cirq, - "module": "qdk.cirq", - "post_assert": lambda mod: hasattr(mod, "__doc__"), - }, } -@pytest.mark.parametrize("name,spec", EXTRAS.items()) -def test_direct_import_with_mock(name, spec): +@pytest.mark.parametrize("name,spec", MOCK_EXTRAS.items()) +def test_reexport_shim_with_mock(name, spec): created = spec["mock"]() try: imported = importlib.import_module(spec["module"]) diff --git a/source/qdk_package/tests/reexports/test_reexports.py b/source/qdk_package/tests/reexports/test_reexports.py new file mode 100644 index 0000000000..ac6e0049fe --- /dev/null +++ b/source/qdk_package/tests/reexports/test_reexports.py @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Tests for qdk re-export shims. + +Only ``qdk.widgets`` and ``qdk.azure`` are re-export shims wrapping third-party +packages. All other qdk submodules (estimator, openqasm, qiskit, cirq, qre, +etc.) now own their code directly and are covered by functional tests elsewhere. +""" + +import importlib +import pytest + + +# ---- Friendly error messages when optional deps are missing ---- + +_REEXPORT_SHIMS = { + "qdk.widgets": {"dep": "qsharp_widgets", "hint": r"pip install qdk\[jupyter\]"}, + "qdk.azure": {"dep": "azure.quantum", "hint": r"pip install qdk\[azure\]"}, +} + + +@pytest.mark.parametrize("mod,spec", _REEXPORT_SHIMS.items()) +def test_missing_optional_gives_helpful_error(mod, spec): + """When the upstream dep is absent, importing the shim should raise + ImportError containing a pip-install hint.""" + try: + importlib.import_module(spec["dep"]) + pytest.skip(f"{spec['dep']} is installed; cannot test missing-dep path") + except ImportError: + pass + + with pytest.raises(ImportError, match=spec["hint"]): + importlib.import_module(mod) diff --git a/source/pip/tests/test_adaptive_cpu_bytecode.py b/source/qdk_package/tests/test_adaptive_cpu_bytecode.py similarity index 99% rename from source/pip/tests/test_adaptive_cpu_bytecode.py rename to source/qdk_package/tests/test_adaptive_cpu_bytecode.py index 632bc69564..c0b9986401 100644 --- a/source/pip/tests/test_adaptive_cpu_bytecode.py +++ b/source/qdk_package/tests/test_adaptive_cpu_bytecode.py @@ -15,11 +15,12 @@ from collections import Counter import pytest -from qsharp._simulation import run_qir, NoiseConfig, Result -import qsharp.openqasm +from qdk.simulation import run_qir, NoiseConfig +from qdk.simulation._simulation import Result +import qdk +import qdk.openqasm from typing import Literal - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -1704,10 +1705,10 @@ def _run_openqasm( sim_type: Literal["clifford", "cpu"] = "cpu", ): """Compile OpenQASM source via the adaptive pass and run on the given simulator.""" - qir = qsharp.openqasm.compile( + qir = qdk.openqasm.compile( qasm_src, - output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, - target_profile=qsharp.TargetProfile.Adaptive_RIF, + output_semantics=qdk.openqasm.OutputSemantics.OpenQasm, + target_profile=qdk.TargetProfile.Adaptive_RIF, ) results = run_qir(qir, shots, seed=seed, type=sim_type) return [map_result_list_to_str(r) for r in results] diff --git a/source/pip/tests/test_adaptive_cpu_noise.py b/source/qdk_package/tests/test_adaptive_cpu_noise.py similarity index 93% rename from source/pip/tests/test_adaptive_cpu_noise.py rename to source/qdk_package/tests/test_adaptive_cpu_noise.py index 303c56e2ab..5e7bed7761 100644 --- a/source/pip/tests/test_adaptive_cpu_noise.py +++ b/source/qdk_package/tests/test_adaptive_cpu_noise.py @@ -11,13 +11,15 @@ """ from collections import Counter +from pathlib import Path from typing import Optional, List import pytest -from qsharp._simulation import run_qir, NoiseConfig, Result -import qsharp.openqasm +from qdk.simulation import run_qir, NoiseConfig +from qdk.simulation._simulation import Result +import qdk +import qdk.openqasm from typing import Literal - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -231,10 +233,10 @@ def test_probabilistic_x_noise(sim_type): bit[3] res = measure qs; """ -QIR_WITH_CORRELATED_NOISE = qsharp.openqasm.compile( +QIR_WITH_CORRELATED_NOISE = qdk.openqasm.compile( QASM_WITH_CORRELATED_NOISE, - output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, - target_profile=qsharp.TargetProfile.Adaptive_RIF, + output_semantics=qdk.openqasm.OutputSemantics.OpenQasm, + target_profile=qdk.TargetProfile.Adaptive_RIF, ) @@ -256,7 +258,7 @@ def test_noise_intrinsics_noisy(sim_type): @pytest.mark.parametrize("sim_type", SIM_TYPES) def test_noise_intrinsics_load_csv_dir(sim_type): noise = NoiseConfig() - noise.load_csv_dir("./csv_dir_test") + noise.load_csv_dir(str(Path(__file__).parent / "csv_dir_test")) output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type=sim_type) assert output == [[Result.One, Result.Zero, Result.One]] @@ -335,10 +337,10 @@ def test_noise_intrinsics_with_registers_noisy(sim_type): bit res = measure q; """ -QIR_NOISE_1Q = qsharp.openqasm.compile( +QIR_NOISE_1Q = qdk.openqasm.compile( QASM_NOISE_1Q, - output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, - target_profile=qsharp.TargetProfile.Adaptive_RIF, + output_semantics=qdk.openqasm.OutputSemantics.OpenQasm, + target_profile=qdk.TargetProfile.Adaptive_RIF, ) @@ -364,10 +366,10 @@ def test_noise_intrinsic_1q_x_flip(sim_type): bit[2] res = measure qs; """ -QIR_NOISE_2Q = qsharp.openqasm.compile( +QIR_NOISE_2Q = qdk.openqasm.compile( QASM_NOISE_2Q, - output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, - target_profile=qsharp.TargetProfile.Adaptive_RIF, + output_semantics=qdk.openqasm.OutputSemantics.OpenQasm, + target_profile=qdk.TargetProfile.Adaptive_RIF, ) @@ -395,10 +397,10 @@ def test_noise_intrinsic_2q_xx_flip(sim_type): bit[5] res = measure qs; """ -QIR_NOISE_5Q = qsharp.openqasm.compile( +QIR_NOISE_5Q = qdk.openqasm.compile( QASM_NOISE_5Q, - output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, - target_profile=qsharp.TargetProfile.Adaptive_RIF, + output_semantics=qdk.openqasm.OutputSemantics.OpenQasm, + target_profile=qdk.TargetProfile.Adaptive_RIF, ) diff --git a/source/pip/tests/test_adaptive_cpu_quantum_ops.py b/source/qdk_package/tests/test_adaptive_cpu_quantum_ops.py similarity index 99% rename from source/pip/tests/test_adaptive_cpu_quantum_ops.py rename to source/qdk_package/tests/test_adaptive_cpu_quantum_ops.py index 0a119f7a92..2f10643f58 100644 --- a/source/pip/tests/test_adaptive_cpu_quantum_ops.py +++ b/source/qdk_package/tests/test_adaptive_cpu_quantum_ops.py @@ -14,7 +14,8 @@ from collections import Counter import pytest -from qsharp._simulation import run_qir, Result +from qdk.simulation import run_qir +from qdk.simulation._simulation import Result from typing import Literal SIM_TYPES = ["cpu", "clifford"] diff --git a/source/pip/tests/test_adaptive_gpu_bytecode.py b/source/qdk_package/tests/test_adaptive_gpu_bytecode.py similarity index 99% rename from source/pip/tests/test_adaptive_gpu_bytecode.py rename to source/qdk_package/tests/test_adaptive_gpu_bytecode.py index 195aec5262..4956fb139c 100644 --- a/source/pip/tests/test_adaptive_gpu_bytecode.py +++ b/source/qdk_package/tests/test_adaptive_gpu_bytecode.py @@ -17,7 +17,7 @@ import sys from collections import Counter import pytest -import qsharp.openqasm +import qdk.openqasm # Skip the whole module when GPU tests aren't requested. if not os.environ.get("QDK_GPU_TESTS"): @@ -27,7 +27,7 @@ GPU_AVAILABLE = False try: - from qsharp._native import try_create_gpu_adapter + from qdk._native import try_create_gpu_adapter gpu_info = try_create_gpu_adapter() print(f"*** USING GPU: {gpu_info}", file=sys.stderr) @@ -35,7 +35,7 @@ except OSError as e: SKIP_REASON = str(e) -from qsharp._simulation import GpuSimulator, NoiseConfig, Result, run_qir +from qdk.simulation._simulation import GpuSimulator, NoiseConfig, Result, run_qir # --------------------------------------------------------------------------- # Helpers @@ -1762,10 +1762,10 @@ def test_call_stack_overflow_guard(): def _run_openqasm(qasm_src: str, shots: int = SHOTS, seed: int = 42): """Compile OpenQASM source via the adaptive pass and run on the GPU.""" global sim - qir = qsharp.openqasm.compile( + qir = qdk.openqasm.compile( qasm_src, - output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, - target_profile=qsharp.TargetProfile.Adaptive_RIF, + output_semantics=qdk.openqasm.OutputSemantics.OpenQasm, + target_profile=qdk.TargetProfile.Adaptive_RIF, ) sim.set_program(qir) return sim.run_shots(shots, seed=seed) diff --git a/source/pip/tests/test_adaptive_gpu_noise.py b/source/qdk_package/tests/test_adaptive_gpu_noise.py similarity index 94% rename from source/pip/tests/test_adaptive_gpu_noise.py rename to source/qdk_package/tests/test_adaptive_gpu_noise.py index d3795db810..0b7eec71fd 100644 --- a/source/pip/tests/test_adaptive_gpu_noise.py +++ b/source/qdk_package/tests/test_adaptive_gpu_noise.py @@ -18,7 +18,7 @@ from collections import Counter import pytest from typing import Optional, List -import qsharp.openqasm +import qdk.openqasm # Skip the whole module when GPU tests aren't requested. if not os.environ.get("QDK_GPU_TESTS"): @@ -28,7 +28,7 @@ GPU_AVAILABLE = False try: - from qsharp._native import try_create_gpu_adapter + from qdk._native import try_create_gpu_adapter gpu_info = try_create_gpu_adapter() print(f"*** USING GPU: {gpu_info}", file=sys.stderr) @@ -36,7 +36,8 @@ except OSError as e: SKIP_REASON = str(e) -from qsharp._simulation import run_qir, GpuSimulator, NoiseConfig, Result +from qdk.simulation import run_qir, NoiseConfig +from qdk.simulation._simulation import GpuSimulator, Result # --------------------------------------------------------------------------- # Helpers @@ -247,10 +248,10 @@ def test_probabilistic_x_noise(): bit[3] res = measure qs; """ -QIR_WITH_CORRELATED_NOISE = qsharp.openqasm.compile( +QIR_WITH_CORRELATED_NOISE = qdk.openqasm.compile( QASM_WITH_CORRELATED_NOISE, - output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, - target_profile=qsharp.TargetProfile.Adaptive_RIF, + output_semantics=qdk.openqasm.OutputSemantics.OpenQasm, + target_profile=qdk.TargetProfile.Adaptive_RIF, ) @@ -360,10 +361,10 @@ def test_noise_intrinsics_with_registers_noisy(): bit res = measure q; """ -QIR_NOISE_1Q = qsharp.openqasm.compile( +QIR_NOISE_1Q = qdk.openqasm.compile( QASM_NOISE_1Q, - output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, - target_profile=qsharp.TargetProfile.Adaptive_RIF, + output_semantics=qdk.openqasm.OutputSemantics.OpenQasm, + target_profile=qdk.TargetProfile.Adaptive_RIF, ) @@ -389,10 +390,10 @@ def test_noise_intrinsic_1q_x_flip(): bit[2] res = measure qs; """ -QIR_NOISE_2Q = qsharp.openqasm.compile( +QIR_NOISE_2Q = qdk.openqasm.compile( QASM_NOISE_2Q, - output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, - target_profile=qsharp.TargetProfile.Adaptive_RIF, + output_semantics=qdk.openqasm.OutputSemantics.OpenQasm, + target_profile=qdk.TargetProfile.Adaptive_RIF, ) @@ -420,10 +421,10 @@ def test_noise_intrinsic_2q_xx_flip(): bit[5] res = measure qs; """ -QIR_NOISE_5Q = qsharp.openqasm.compile( +QIR_NOISE_5Q = qdk.openqasm.compile( QASM_NOISE_5Q, - output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, - target_profile=qsharp.TargetProfile.Adaptive_RIF, + output_semantics=qdk.openqasm.OutputSemantics.OpenQasm, + target_profile=qdk.TargetProfile.Adaptive_RIF, ) diff --git a/source/pip/tests/test_adaptive_gpu_quantum_ops.py b/source/qdk_package/tests/test_adaptive_gpu_quantum_ops.py similarity index 99% rename from source/pip/tests/test_adaptive_gpu_quantum_ops.py rename to source/qdk_package/tests/test_adaptive_gpu_quantum_ops.py index 01613fdc3a..186581d925 100644 --- a/source/pip/tests/test_adaptive_gpu_quantum_ops.py +++ b/source/qdk_package/tests/test_adaptive_gpu_quantum_ops.py @@ -26,7 +26,7 @@ GPU_AVAILABLE = False try: - from qsharp._native import try_create_gpu_adapter + from qdk._native import try_create_gpu_adapter gpu_info = try_create_gpu_adapter() print(f"*** USING GPU: {gpu_info}", file=sys.stderr) @@ -34,7 +34,7 @@ except OSError as e: SKIP_REASON = str(e) -from qsharp._simulation import GpuSimulator, Result +from qdk.simulation._simulation import GpuSimulator, Result def map_result_list_to_str(results): diff --git a/source/pip/tests/test_adaptive_pass.py b/source/qdk_package/tests/test_adaptive_pass.py similarity index 99% rename from source/pip/tests/test_adaptive_pass.py rename to source/qdk_package/tests/test_adaptive_pass.py index fe6b38815b..f4d744657e 100644 --- a/source/pip/tests/test_adaptive_pass.py +++ b/source/qdk_package/tests/test_adaptive_pass.py @@ -12,9 +12,8 @@ import pyqir import pytest -from qsharp._adaptive_pass import AdaptiveProfilePass, AdaptiveProgram, Bytecode -from qsharp._adaptive_bytecode import * - +from qdk._adaptive_pass import AdaptiveProfilePass, AdaptiveProgram, Bytecode +from qdk._adaptive_bytecode import * # --------------------------------------------------------------------------- # Helpers diff --git a/source/pip/tests/test_callable_passing.py b/source/qdk_package/tests/test_callable_passing.py similarity index 94% rename from source/pip/tests/test_callable_passing.py rename to source/qdk_package/tests/test_callable_passing.py index c02c37028c..97a4d4891f 100644 --- a/source/pip/tests/test_callable_passing.py +++ b/source/qdk_package/tests/test_callable_passing.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import qsharp +import qdk as qsharp from expecttest import assert_expected_inline from textwrap import dedent @@ -16,7 +16,7 @@ def test_python_callable_passed_to_python_callable() -> None: x + 1 } """) - from qsharp.code import InvokeWithFive, AddOne + from qdk.code import InvokeWithFive, AddOne assert InvokeWithFive(AddOne) == 6 @@ -30,7 +30,7 @@ def test_python_callable_passed_to_qsharp_callable() -> None: x + 1 } """) - from qsharp.code import InvokeWithFive + from qdk.code import InvokeWithFive f = qsharp.eval("AddOne") assert InvokeWithFive(f) == 6 @@ -61,7 +61,7 @@ def test_run_qsharp_callable_passed_to_python_callable() -> None: x + 1 } """) - from qsharp.code import InvokeWithFive + from qdk.code import InvokeWithFive add_one = qsharp.eval("AddOne") res = qsharp.run(InvokeWithFive, 1, add_one)[0] assert res == 6 @@ -81,7 +81,7 @@ def test_python_callable_with_unsupported_types_passed_to_python_callable() -> N sum } """) - from qsharp.code import MakeRange, SumRangeFromMaker + from qdk.code import MakeRange, SumRangeFromMaker assert SumRangeFromMaker(MakeRange) == 55 @@ -95,7 +95,7 @@ def test_qsharp_closure_from_python_callable_passed_to_python_callable() -> None x -> x + inc } """) - from qsharp.code import InvokeWithFive, MakeAdd + from qdk.code import InvokeWithFive, MakeAdd assert InvokeWithFive(MakeAdd(1)) == 6 @@ -110,7 +110,7 @@ def test_qir_from_python_callable_passed_to_python_callable() -> None: ApplyToEach(H, qs); } """) - from qsharp.code import InvokeWithQubits, AllH + from qdk.code import InvokeWithQubits, AllH qir = qsharp.compile(InvokeWithQubits, 3, AllH) assert_expected_inline(str(qir), """\ %Result = type opaque @@ -159,7 +159,7 @@ def test_qir_from_qsharp_callable_passed_to_python_callable() -> None: ApplyToEach(H, qs); } """) - from qsharp.code import InvokeWithQubits + from qdk.code import InvokeWithQubits all_h = qsharp.eval("AllH") qir = qsharp.compile(InvokeWithQubits, 3, all_h) assert_expected_inline(str(qir), """\ @@ -206,7 +206,7 @@ def test_qir_from_qsharp_closure_passed_to_python_callable() -> None: f(qs) } """) - from qsharp.code import InvokeWithQubits + from qdk.code import InvokeWithQubits apply_h = qsharp.eval("ApplyToEach(H, _)") qir = qsharp.compile(InvokeWithQubits, 3, apply_h) assert_expected_inline(str(qir), """\ @@ -256,7 +256,7 @@ def test_circuit_from_python_callable_passed_to_python_callable() -> None: ApplyToEach(H, qs); } """) - from qsharp.code import InvokeWithQubits, AllH + from qdk.code import InvokeWithQubits, AllH circuit = qsharp.circuit(InvokeWithQubits, 3, AllH) assert_expected_inline(str(circuit), """q_0 ── H ── q_1 ── H ── @@ -275,7 +275,7 @@ def test_circuit_from_qsharp_callable_passed_to_python_callable() -> None: ApplyToEach(H, qs); } """) - from qsharp.code import InvokeWithQubits + from qdk.code import InvokeWithQubits all_h = qsharp.eval("AllH") circuit = qsharp.circuit(InvokeWithQubits, 3, all_h) assert_expected_inline(str(circuit), """q_0 ── H ── @@ -292,7 +292,7 @@ def test_circuit_from_qsharp_closure_passed_to_python_callable() -> None: f(qs) } """) - from qsharp.code import InvokeWithQubits + from qdk.code import InvokeWithQubits apply_h = qsharp.eval("ApplyToEach(H, _)") circuit = qsharp.circuit(InvokeWithQubits, 3, apply_h) assert_expected_inline(str(circuit), """q_0 ── H ── diff --git a/source/pip/tests/test_clifford_simulator.py b/source/qdk_package/tests/test_clifford_simulator.py similarity index 94% rename from source/pip/tests/test_clifford_simulator.py rename to source/qdk_package/tests/test_clifford_simulator.py index 40a1ddaffa..6cdcbb6a13 100644 --- a/source/pip/tests/test_clifford_simulator.py +++ b/source/qdk_package/tests/test_clifford_simulator.py @@ -4,12 +4,13 @@ from pathlib import Path import pyqir -import qsharp -from qsharp._simulation import run_qir_clifford, NoiseConfig -from qsharp._device._atom import NeutralAtomDevice -from qsharp._device._atom._decomp import DecomposeRzAnglesToCliffordGates -from qsharp._device._atom._validate import ValidateNoConditionalBranches -from qsharp import TargetProfile, Result +import qdk as qsharp +from qdk.simulation import NoiseConfig +from qdk.simulation._simulation import run_qir_clifford +from qdk._device._atom import NeutralAtomDevice +from qdk._device._atom._decomp import DecomposeRzAnglesToCliffordGates +from qdk._device._atom._validate import ValidateNoConditionalBranches +from qdk import TargetProfile, Result current_file_path = Path(__file__) # Get the directory of the current file @@ -71,8 +72,7 @@ def test_million(): def test_program_with_branching_succeeds(): qsharp.init(target_profile=TargetProfile.Adaptive_RI) - qsharp.eval( - """ + qsharp.eval(""" operation Main() : Result { use q = Qubit(); H(q); @@ -81,8 +81,7 @@ def test_program_with_branching_succeeds(): } return MResetZ(q); } - """ - ) + """) ir = qsharp.compile("Main()") results = run_qir_clifford(str(ir), 1, NoiseConfig()) assert len(results) == 1 diff --git a/source/pip/tests/test_correlated_noise.py b/source/qdk_package/tests/test_correlated_noise.py similarity index 85% rename from source/pip/tests/test_correlated_noise.py rename to source/qdk_package/tests/test_correlated_noise.py index 123f49fca0..0a330bba29 100644 --- a/source/pip/tests/test_correlated_noise.py +++ b/source/qdk_package/tests/test_correlated_noise.py @@ -3,16 +3,17 @@ import pytest import sys -from qsharp._simulation import NoiseConfig, run_qir -from qsharp import Result -import qsharp.openqasm +from pathlib import Path +from qdk.simulation import NoiseConfig, run_qir +from qdk import Result +import qdk.openqasm SKIP_REASON = "GPU is not available" gpu_info = "Unknown" try: - from qsharp._native import try_create_gpu_adapter + from qdk._native import try_create_gpu_adapter gpu_info = try_create_gpu_adapter() # Printing to stderr so that it is visible if CI run fails @@ -37,10 +38,10 @@ bit[3] res = measure qs; """ -QIR_WITH_CORRELATED_NOISE = qsharp.openqasm.compile( +QIR_WITH_CORRELATED_NOISE = qdk.openqasm.compile( QASM_WITH_CORRELATED_NOISE, - output_semantics=qsharp.openqasm.OutputSemantics.OpenQasm, - target_profile=qsharp.TargetProfile.Base, + output_semantics=qdk.openqasm.OutputSemantics.OpenQasm, + target_profile=qdk.TargetProfile.Base, ) @@ -76,7 +77,7 @@ def test_noisy_simulation_gpu(): def test_load_csv_dir(): noise = NoiseConfig() - noise.load_csv_dir("./csv_dir_test") + noise.load_csv_dir(str(Path(__file__).parent / "csv_dir_test")) for type in CPU_SIMULATORS: output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type=type) assert output == [[Result.One, Result.Zero, Result.One]] @@ -85,7 +86,7 @@ def test_load_csv_dir(): @pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON) def test_load_csv_dir_gpu(): noise = NoiseConfig() - noise.load_csv_dir("./csv_dir_test") + noise.load_csv_dir(str(Path(__file__).parent / "csv_dir_test")) output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type="gpu") assert output == [[Result.One, Result.Zero, Result.One]] diff --git a/source/pip/tests/test_cpu_simulator.py b/source/qdk_package/tests/test_cpu_simulator.py similarity index 98% rename from source/pip/tests/test_cpu_simulator.py rename to source/qdk_package/tests/test_cpu_simulator.py index d6d611d699..6ddf6f17ce 100644 --- a/source/pip/tests/test_cpu_simulator.py +++ b/source/qdk_package/tests/test_cpu_simulator.py @@ -9,13 +9,14 @@ import pytest -from qsharp._native import Result +from qdk._native import Result -import qsharp -from qsharp import TargetProfile -from qsharp import openqasm +import qdk as qsharp +from qdk import TargetProfile +from qdk import openqasm -from qsharp._simulation import run_qir_cpu, NoiseConfig +from qdk.simulation import NoiseConfig +from qdk.simulation._simulation import run_qir_cpu current_file_path = Path(__file__) # Get the directory of the current file @@ -44,16 +45,14 @@ def result_array_to_string(results: Sequence[Result]) -> str: def test_cpu_seeding_no_noise(): qsharp.init(target_profile=TargetProfile.Base) - qsharp.eval( - """ + qsharp.eval(""" operation BellTest() : Result[] { use qs = Qubit[2]; H(qs[0]); CNOT(qs[0], qs[1]); MResetEachZ(qs) } - """ - ) + """) qir = str(qsharp.compile("BellTest()")) diff --git a/source/pip/tests/test_enums.py b/source/qdk_package/tests/test_enums.py similarity index 95% rename from source/pip/tests/test_enums.py rename to source/qdk_package/tests/test_enums.py index 0fb36f5e4f..937c8ca891 100644 --- a/source/pip/tests/test_enums.py +++ b/source/qdk_package/tests/test_enums.py @@ -3,17 +3,16 @@ from textwrap import dedent import pytest -import qsharp -import qsharp.code -import qsharp.utils +import qdk as qsharp +import qdk.code from contextlib import redirect_stdout import io -from qsharp import TargetProfile +from qdk import TargetProfile # pull in from native module for tests so that we don't have to install qiskit # using the interop module -from qsharp._native import OutputSemantics, ProgramType +from qdk._native import OutputSemantics, ProgramType def test_target_profile_int_values_match_enum_values() -> None: diff --git a/source/pip/tests/test_generic_estimator.py b/source/qdk_package/tests/test_generic_estimator.py similarity index 99% rename from source/pip/tests/test_generic_estimator.py rename to source/qdk_package/tests/test_generic_estimator.py index c8a50bab22..a0e64f5a3a 100644 --- a/source/pip/tests/test_generic_estimator.py +++ b/source/qdk_package/tests/test_generic_estimator.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. import pytest -import qsharp +import qdk as qsharp class SampleAlgorithm: diff --git a/source/pip/tests/test_gpu_simulator.py b/source/qdk_package/tests/test_gpu_simulator.py similarity index 98% rename from source/pip/tests/test_gpu_simulator.py rename to source/qdk_package/tests/test_gpu_simulator.py index 013dd808e2..d9290689fc 100644 --- a/source/pip/tests/test_gpu_simulator.py +++ b/source/qdk_package/tests/test_gpu_simulator.py @@ -11,7 +11,7 @@ import pytest import sys -from qsharp._native import Result +from qdk._native import Result # Skip all tests in this module if QDK_GPU_TESTS is not set if not os.environ.get("QDK_GPU_TESTS"): @@ -22,7 +22,7 @@ gpu_info = "Unknown" try: - from qsharp._native import try_create_gpu_adapter + from qdk._native import try_create_gpu_adapter gpu_info = try_create_gpu_adapter() # Printing to stderr so that it is visible if CI run fails @@ -34,11 +34,12 @@ SKIP_REASON = str(e) -import qsharp -from qsharp import TargetProfile -from qsharp import openqasm +import qdk as qsharp +from qdk import TargetProfile +from qdk import openqasm -from qsharp._simulation import run_qir_gpu, NoiseConfig +from qdk.simulation import NoiseConfig +from qdk.simulation._simulation import run_qir_gpu current_file_path = Path(__file__) # Get the directory of the current file @@ -68,16 +69,14 @@ def result_array_to_string(results: Sequence[Result]) -> str: @pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON) def test_gpu_seeding_no_noise(): qsharp.init(target_profile=TargetProfile.Base) - qsharp.eval( - """ + qsharp.eval(""" operation BellTest() : Result[] { use qs = Qubit[2]; H(qs[0]); CNOT(qs[0], qs[1]); MResetEachZ(qs) } - """ - ) + """) qir = str(qsharp.compile("BellTest()")) @@ -520,8 +519,7 @@ def test_gpu_mz_idempotent_noiseless(): """MZ (measure without reset) should be idempotent: two consecutive measurements on the same qubit must always agree.""" qsharp.init(target_profile=TargetProfile.Base) - qsharp.eval( - """ + qsharp.eval(""" operation MzIdempotent() : Result[] { use q = Qubit(); H(q); @@ -529,8 +527,7 @@ def test_gpu_mz_idempotent_noiseless(): let r1 = M(q); [r0, r1] } - """ - ) + """) qir = str(qsharp.compile("MzIdempotent()")) shots = 1000 @@ -563,8 +560,7 @@ def test_gpu_reset_preserves_distribution(): After CNOT the state is cos(π/12)|00⟩ + sin(π/12)|11⟩. Reset on q0 collapses it, leaving q1 with the same skewed distribution.""" qsharp.init(target_profile=TargetProfile.Base) - qsharp.eval( - """ + qsharp.eval(""" operation ResetPreservesDistribution() : Result[] { use qs = Qubit[2]; Rx(Std.Math.PI() / 6.0, qs[0]); @@ -572,8 +568,7 @@ def test_gpu_reset_preserves_distribution(): Reset(qs[0]); MResetEachZ(qs) } - """ - ) + """) qir = str(qsharp.compile("ResetPreservesDistribution()")) shots = 1000 diff --git a/source/pip/tests/test_interpreter.py b/source/qdk_package/tests/test_interpreter.py similarity index 97% rename from source/pip/tests/test_interpreter.py rename to source/qdk_package/tests/test_interpreter.py index d9c4e365ab..5b85b3b697 100644 --- a/source/pip/tests/test_interpreter.py +++ b/source/qdk_package/tests/test_interpreter.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. from textwrap import dedent -from qsharp._native import ( +from qdk._native import ( Interpreter, Result, Pauli, @@ -10,7 +10,7 @@ TargetProfile, CircuitConfig, ) -from qsharp._qsharp import qsharp_value_to_python_value +from qdk._interpreter import qsharp_value_to_python_value import pytest from expecttest import assert_expected_inline @@ -452,41 +452,33 @@ def callback(output): def test_dump_circuit() -> None: e = Interpreter(TargetProfile.Unrestricted, trace_circuit=True) - e.interpret( - """ + e.interpret(""" use q1 = Qubit(); use q2 = Qubit(); X(q1); - """ - ) + """) circuit = e.dump_circuit() - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── X ── q_1 ─────── - """ - ) + """) e.interpret("X(q2);") circuit = e.dump_circuit() - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── X ── q_1 ── X ── - """ - ) + """) def test_entry_expr_circuit() -> None: e = Interpreter(TargetProfile.Unrestricted) e.interpret("operation Foo() : Result { use q = Qubit(); H(q); return M(q) }") circuit = e.circuit(CircuitConfig(), "Foo()") - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── H ──── M ── ╘═══ - """ - ) + """) def test_swap_label_circuit() -> None: @@ -495,12 +487,10 @@ def test_swap_label_circuit() -> None: "operation Foo() : Unit { use q1 = Qubit(); use q2 = Qubit(); X(q1); Relabel([q1, q2], [q2, q1]); X(q2); }" ) circuit = e.circuit(CircuitConfig(), "Foo()") - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── X ──── X ── q_1 ────────────── - """ - ) + """) def test_callables_failing_profile_validation_are_not_registered() -> None: @@ -581,7 +571,9 @@ def test_adaptive_ri_qir_can_be_generated() -> None: e = Interpreter(TargetProfile.Adaptive_RI) e.interpret(adaptive_input) qir = e.qir("Test.Main()") - assert_expected_inline(qir, """\ + assert_expected_inline( + qir, + """\ %Result = type opaque %Qubit = type opaque @@ -618,7 +610,8 @@ def test_adaptive_ri_qir_can_be_generated() -> None: !2 = !{i32 1, !"dynamic_qubit_management", i1 false} !3 = !{i32 1, !"dynamic_result_management", i1 false} !4 = !{i32 5, !"int_computations", !{!"i64"}} -""") +""", + ) def test_base_qir_can_be_generated() -> None: @@ -642,7 +635,9 @@ def test_base_qir_can_be_generated() -> None: e = Interpreter(TargetProfile.Base) e.interpret(base_input) qir = e.qir("Test.Main()") - assert_expected_inline(qir, """\ + assert_expected_inline( + qir, + """\ %Result = type opaque %Qubit = type opaque @@ -678,19 +673,18 @@ def test_base_qir_can_be_generated() -> None: !1 = !{i32 7, !"qir_minor_version", i32 0} !2 = !{i32 1, !"dynamic_qubit_management", i1 false} !3 = !{i32 1, !"dynamic_result_management", i1 false} -""") +""", + ) def test_operation_circuit() -> None: e = Interpreter(TargetProfile.Unrestricted) e.interpret("operation Foo(q: Qubit) : Result { H(q); return M(q) }") circuit = e.circuit(CircuitConfig(), operation="Foo") - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── H ──── M ── ╘═══ - """ - ) + """) def test_unsupported_operation_circuit() -> None: diff --git a/source/pip/tests/test_noisy_config.py b/source/qdk_package/tests/test_noisy_config.py similarity index 98% rename from source/pip/tests/test_noisy_config.py rename to source/qdk_package/tests/test_noisy_config.py index 1042b21f31..720b9a7a5d 100644 --- a/source/pip/tests/test_noisy_config.py +++ b/source/qdk_package/tests/test_noisy_config.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from qsharp._simulation import NoiseConfig +from qdk.simulation import NoiseConfig import pytest diff --git a/source/pip/tests/test_noisy_simulator.py b/source/qdk_package/tests/test_noisy_simulator.py similarity index 99% rename from source/pip/tests/test_noisy_simulator.py rename to source/qdk_package/tests/test_noisy_simulator.py index 5bc9f301d8..b2a2e6aaec 100644 --- a/source/pip/tests/test_noisy_simulator.py +++ b/source/qdk_package/tests/test_noisy_simulator.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from qsharp.noisy_simulator import ( +from qdk.simulation import ( NoisySimulatorError, Operation, Instrument, @@ -10,7 +10,6 @@ ) import pytest - # Tests for the Q# noisy simulator. diff --git a/source/pip/tests/test_project.py b/source/qdk_package/tests/test_project.py similarity index 99% rename from source/pip/tests/test_project.py rename to source/qdk_package/tests/test_project.py index 321acef265..872520862c 100644 --- a/source/pip/tests/test_project.py +++ b/source/qdk_package/tests/test_project.py @@ -7,9 +7,9 @@ @pytest.fixture def qsharp(): - import qsharp - import qsharp._fs - import qsharp._http + import qdk as qsharp + import qdk._fs + import qdk._http qsharp._fs.read_file = read_file_memfs qsharp._fs.list_directory = list_directory_memfs diff --git a/source/pip/tests/test_qasm.py b/source/qdk_package/tests/test_qasm.py similarity index 99% rename from source/pip/tests/test_qasm.py rename to source/qdk_package/tests/test_qasm.py index 7ddb4d0c54..f5b1f0c239 100644 --- a/source/pip/tests/test_qasm.py +++ b/source/qdk_package/tests/test_qasm.py @@ -4,7 +4,7 @@ from math import pi from textwrap import dedent import pytest -from qsharp import ( +from qdk import ( init, TargetProfile, BitFlipNoise, @@ -17,8 +17,8 @@ circuit as qsharp_circuit, estimate as qsharp_estimate, ) -from qsharp.estimator import EstimatorParams, QubitParams, QECScheme, LogicalCounts -from qsharp.openqasm import ( +from qdk.estimator import EstimatorParams, QubitParams, QECScheme, LogicalCounts +from qdk.openqasm import ( import_openqasm, run, compile, @@ -27,7 +27,7 @@ ProgramType, QasmError, ) -import qsharp.code as code +import qdk.code as code # Run diff --git a/source/pip/tests/test_qasm_io.py b/source/qdk_package/tests/test_qasm_io.py similarity index 99% rename from source/pip/tests/test_qasm_io.py rename to source/qdk_package/tests/test_qasm_io.py index 8630b9982b..c2ce5c4f2c 100644 --- a/source/pip/tests/test_qasm_io.py +++ b/source/qdk_package/tests/test_qasm_io.py @@ -3,17 +3,17 @@ from math import pi import pytest -from qsharp import ( +from qdk import ( QSharpError, init, TargetProfile, Result, ) -from qsharp.openqasm import ( +from qdk.openqasm import ( import_openqasm, ProgramType, ) -import qsharp.code as code +import qdk.code as code def test_import_unsupported_angle_input_type() -> None: diff --git a/source/pip/tests/test_qsharp.py b/source/qdk_package/tests/test_qsharp.py similarity index 95% rename from source/pip/tests/test_qsharp.py rename to source/qdk_package/tests/test_qsharp.py index 0d1fd3c55f..cec74ed94d 100644 --- a/source/pip/tests/test_qsharp.py +++ b/source/qdk_package/tests/test_qsharp.py @@ -3,9 +3,8 @@ from textwrap import dedent import pytest -import qsharp -import qsharp.code -import qsharp.utils +import qdk as qsharp +import qdk.code from contextlib import redirect_stdout import io @@ -26,13 +25,11 @@ def test_stdout_multiple_lines() -> None: qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted) f = io.StringIO() with redirect_stdout(f): - qsharp.eval( - """ + qsharp.eval(""" use q = Qubit(); Microsoft.Quantum.Diagnostics.DumpMachine(); Message("Hello!"); - """ - ) + """) assert f.getvalue() == "STATE:\n|0⟩: 1.0000+0.0000𝑖\nHello!\n" @@ -107,13 +104,11 @@ def test_classical_seed() -> None: def test_dump_machine() -> None: qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted) - qsharp.eval( - """ + qsharp.eval(""" use q1 = Qubit(); use q2 = Qubit(); X(q1); - """ - ) + """) state_dump = qsharp.dump_machine() assert state_dump.qubit_count == 2 assert len(state_dump) == 1 @@ -170,24 +165,24 @@ def test_dump_machine() -> None: def test_dump_operation() -> None: qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted) - res = qsharp.utils.dump_operation("qs => ()", 1) + res = qsharp.dump_operation("qs => ()", 1) assert res == [ [complex(1.0, 0.0), complex(0.0, 0.0)], [complex(0.0, 0.0), complex(1.0, 0.0)], ] - res = qsharp.utils.dump_operation("qs => H(qs[0])", 1) + res = qsharp.dump_operation("qs => H(qs[0])", 1) assert res == [ [complex(0.707107, 0.0), complex(0.707107, 0.0)], [complex(0.707107, 0.0), complex(-0.707107, 0.0)], ] - res = qsharp.utils.dump_operation("qs => CNOT(qs[0], qs[1])", 2) + res = qsharp.dump_operation("qs => CNOT(qs[0], qs[1])", 2) assert res == [ [complex(1.0, 0.0), complex(0.0, 0.0), complex(0.0, 0.0), complex(0.0, 0.0)], [complex(0.0, 0.0), complex(1.0, 0.0), complex(0.0, 0.0), complex(0.0, 0.0)], [complex(0.0, 0.0), complex(0.0, 0.0), complex(0.0, 0.0), complex(1.0, 0.0)], [complex(0.0, 0.0), complex(0.0, 0.0), complex(1.0, 0.0), complex(0.0, 0.0)], ] - res = qsharp.utils.dump_operation("qs => CCNOT(qs[0], qs[1], qs[2])", 3) + res = qsharp.dump_operation("qs => CCNOT(qs[0], qs[1], qs[2])", 3) assert res == [ [ complex(1.0, 0.0), @@ -273,14 +268,14 @@ def test_dump_operation() -> None: qsharp.eval( "operation ApplySWAP(qs : Qubit[]) : Unit is Ctl + Adj { SWAP(qs[0], qs[1]); }" ) - res = qsharp.utils.dump_operation("ApplySWAP", 2) + res = qsharp.dump_operation("ApplySWAP", 2) assert res == [ [complex(1.0, 0.0), complex(0.0, 0.0), complex(0.0, 0.0), complex(0.0, 0.0)], [complex(0.0, 0.0), complex(0.0, 0.0), complex(1.0, 0.0), complex(0.0, 0.0)], [complex(0.0, 0.0), complex(1.0, 0.0), complex(0.0, 0.0), complex(0.0, 0.0)], [complex(0.0, 0.0), complex(0.0, 0.0), complex(0.0, 0.0), complex(1.0, 0.0)], ] - res = qsharp.utils.dump_operation("qs => ()", 8) + res = qsharp.dump_operation("qs => ()", 8) for i in range(8): for j in range(8): if i == j: @@ -862,8 +857,8 @@ def test_callables_in_namespaces_exposed_into_env_submodules_and_removed_on_rein qsharp.eval("namespace Test { function Four() : Int { 4 } }") qsharp.eval("function Identity(a : Int) : Int { a }") # should be able to import callables from env and namespace submodule - from qsharp.code import Identity - from qsharp.code.Test import Four + from qdk.code import Identity + from qdk.code.Test import Four assert Identity(4) == 4 assert Four() == 4 @@ -901,12 +896,10 @@ def test_callables_with_unsupported_return_types_raise_errors_on_call() -> None: def test_callable_with_unsupported_udt_type_raises_error_on_call() -> None: qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" newtype Data = (Int, Double); function Unsupported(a : Data) : Unit { } - """ - ) + """) with pytest.raises( qsharp.QSharpError, match='unsupported input type: `UDT<"Data":' ): @@ -915,12 +908,10 @@ def test_callable_with_unsupported_udt_type_raises_error_on_call() -> None: def test_callable_with_unsupported_udt_return_type_raises_error_on_call() -> None: qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" newtype Data = (Int, Double); function Unsupported() : Data { fail "won\'t be called" } - """ - ) + """) with pytest.raises( qsharp.QSharpError, match='unsupported output type: `UDT<"Data":' ): @@ -932,14 +923,12 @@ def test_returning_unsupported_udt_from_eval_raises_error_on_call() -> None: with pytest.raises( TypeError, match="structs with anonymous fields are not supported: Data" ): - qsharp.eval( - """ + qsharp.eval(""" { newtype Data = (Int, Double); Data(2, 3.0) } - """ - ) + """) def test_struct_call_constructor_exposed_into_env() -> None: @@ -951,14 +940,12 @@ def test_struct_call_constructor_exposed_into_env() -> None: def test_udts_are_accepted_as_input() -> None: qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" struct Data { a : Int, b : Int } function SwapData(data : Data) : Data { new Data { a = data.b, b = data.a } } - """ - ) + """) # Dict val = qsharp.code.SwapData({"a": 2, "b": 3}) assert val.a == 3 and val.b == 2 @@ -1014,8 +1001,8 @@ def test_function_defined_before_namespace_keeps_both_accessible() -> None: qsharp.eval("namespace Four { function Two() : Int { 42 } }") assert qsharp.code.Four() == 4 assert qsharp.code.Four.Two() == 42 - from qsharp.code import Four - from qsharp.code.Four import Two + from qdk.code import Four + from qdk.code.Four import Two assert Four() == 4 assert Two() == 42 @@ -1027,8 +1014,8 @@ def test_namespace_defined_before_function_keeps_both_accessible() -> None: qsharp.eval("function Four() : Int { 4 }") assert qsharp.code.Four() == 4 assert qsharp.code.Four.Two() == 42 - from qsharp.code import Four - from qsharp.code.Four import Two + from qdk.code import Four + from qdk.code.Four import Two assert Four() == 4 assert Two() == 42 @@ -1056,72 +1043,59 @@ def test_qsharp_callables_are_not_shadowed() -> None: def test_circuit_from_python_callable() -> None: qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" operation Foo() : Unit { use q1 = Qubit(); use q2 = Qubit(); X(q1); } - """ - ) + """) circuit = qsharp.circuit(qsharp.code.Foo) - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── X ── q_1 ─────── - """ - ) + """) def test_circuit_from_qsharp_callable() -> None: qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" operation Foo() : Unit { use q1 = Qubit(); use q2 = Qubit(); X(q1); } - """ - ) + """) foo = qsharp.eval("Foo") circuit = qsharp.circuit(foo) - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── X ── q_1 ─────── - """ - ) + """) def test_circuit_with_generation_method() -> None: qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" operation Foo() : Unit { use q1 = Qubit(); use q2 = Qubit(); X(q1); Reset(q1); } - """ - ) + """) circuit = qsharp.circuit( qsharp.code.Foo, generation_method=qsharp.CircuitGenerationMethod.Simulate ) - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── X ──── |0〉 ── q_1 ──────────────── - """ - ) + """) def test_circuit_with_static_generation_method() -> None: qsharp.init(target_profile=qsharp.TargetProfile.Adaptive_RIF) - qsharp.eval( - """ + qsharp.eval(""" operation Foo() : Result { use q = Qubit(); H(q); @@ -1130,23 +1104,19 @@ def test_circuit_with_static_generation_method() -> None: Reset(q); r } - """ - ) + """) circuit = qsharp.circuit( "Foo()", generation_method=qsharp.CircuitGenerationMethod.Static ) - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── H ──── M ──── if: c_0 = |1〉 ──── |0〉 ── ╘═══════════ ● ═════════════════ - """ - ) + """) def test_circuit_from_qsharp_callable_static() -> None: qsharp.init(target_profile=qsharp.TargetProfile.Adaptive_RIF) - qsharp.eval( - """ + qsharp.eval(""" operation Foo() : Unit { use q = Qubit(); H(q); @@ -1154,68 +1124,55 @@ def test_circuit_from_qsharp_callable_static() -> None: if r == One { X(q); } Reset(q); } - """ - ) + """) circuit = qsharp.circuit( qsharp.code.Foo, generation_method=qsharp.CircuitGenerationMethod.Static ) - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── H ──── M ──── if: c_0 = |1〉 ──── |0〉 ── ╘═══════════ ● ═════════════════ - """ - ) + """) def test_circuit_from_python_callable_with_args() -> None: qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" operation Foo(nQubits : Int) : Unit { use qs = Qubit[nQubits]; ApplyToEach(X, qs); } - """ - ) + """) circuit = qsharp.circuit(qsharp.code.Foo, 2) - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── X ── q_1 ── X ── - """ - ) + """) def test_circuit_from_qsharp_callable_with_args() -> None: qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" operation Foo(nQubits : Int) : Unit { use qs = Qubit[nQubits]; ApplyToEach(X, qs); } - """ - ) + """) foo = qsharp.eval("Foo") circuit = qsharp.circuit(foo, 2) - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── X ── q_1 ── X ── - """ - ) + """) def test_circuit_with_measure_from_callable() -> None: qsharp.init() qsharp.eval("operation Foo() : Result { use q = Qubit(); H(q); return M(q) }") circuit = qsharp.circuit(qsharp.code.Foo) - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── H ──── M ── ╘═══ - """ - ) + """) def test_swap_label_circuit_from_callable() -> None: @@ -1224,9 +1181,7 @@ def test_swap_label_circuit_from_callable() -> None: "operation Foo() : Unit { use q1 = Qubit(); use q2 = Qubit(); X(q1); Relabel([q1, q2], [q2, q1]); X(q2); }" ) circuit = qsharp.circuit(qsharp.code.Foo) - assert str(circuit) == dedent( - """\ + assert str(circuit) == dedent("""\ q_0 ── X ──── X ── q_1 ────────────── - """ - ) + """) diff --git a/source/pip/tests/test_re.py b/source/qdk_package/tests/test_re.py similarity index 99% rename from source/pip/tests/test_re.py rename to source/qdk_package/tests/test_re.py index 565ef21881..291f2cdb35 100644 --- a/source/pip/tests/test_re.py +++ b/source/qdk_package/tests/test_re.py @@ -1,8 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import qsharp -from qsharp.estimator import EstimatorParams, QubitParams, QECScheme, LogicalCounts +import qdk as qsharp +from qdk.estimator import EstimatorParams, QubitParams, QECScheme, LogicalCounts def test_qsharp_estimation() -> None: diff --git a/source/qdk_package/tests/test_reexports.py b/source/qdk_package/tests/test_reexports.py deleted file mode 100644 index cbaf445a1a..0000000000 --- a/source/qdk_package/tests/test_reexports.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -import pytest, importlib - - -def test_qdk_qsharp_submodule_available(): - # Import the qsharp submodule explicitly. - qdk_qsharp = importlib.import_module("qdk.qsharp") - # Ensure a core API is reachable via submodule - assert hasattr(qdk_qsharp, "run"), "qsharp.run missing in submodule" - - -def test_estimator_and_openqasm_shims(): - est = importlib.import_module("qdk.estimator") - oq = importlib.import_module("qdk.openqasm") - assert hasattr(est, "__doc__") - assert hasattr(oq, "__doc__") - - -def test_missing_optional_direct_imports(): - # If optional extras truly not installed, importing their submodules should raise ImportError. - # We probe without using mocks here. - for mod in ("qdk.widgets", "qdk.azure", "qdk.qiskit", "qdk.cirq", "qdk.qre"): - base_dep = { - "qdk.widgets": "qsharp_widgets", - "qdk.azure": "azure.quantum", - "qdk.qiskit": "qiskit", - "qdk.cirq": "cirq", - "qdk.qre": "qre", - }[mod] - try: - importlib.import_module(base_dep) - dep_installed = True - except Exception: - dep_installed = False - if not dep_installed: - try: - importlib.import_module(mod) - except ImportError as e: - # Expected path: verify helpful hint present - assert "pip install qdk[" in str(e) - else: - # If it imported anyway, treat as environment providing the feature (e.g. via dev install) - pass diff --git a/source/pip/tests/test_simulators_gates_noiseless.py b/source/qdk_package/tests/test_simulators_gates_noiseless.py similarity index 99% rename from source/pip/tests/test_simulators_gates_noiseless.py rename to source/qdk_package/tests/test_simulators_gates_noiseless.py index 976b274539..02a666729b 100644 --- a/source/pip/tests/test_simulators_gates_noiseless.py +++ b/source/qdk_package/tests/test_simulators_gates_noiseless.py @@ -4,12 +4,12 @@ from collections import Counter import os import pytest -import qsharp -from qsharp import compile, Result, TargetProfile -from qsharp._simulation import ( +import qdk as qsharp +from qdk._interpreter import compile +from qdk import Result, TargetProfile +from qdk.simulation import run_qir as _run_qir, NoiseConfig +from qdk.simulation._simulation import ( GpuSimulator, - run_qir as _run_qir, - NoiseConfig, try_create_gpu_adapter, ) from typing import Literal, List, Optional, TypeAlias diff --git a/source/pip/tests/test_simulators_gates_noisy.py b/source/qdk_package/tests/test_simulators_gates_noisy.py similarity index 98% rename from source/pip/tests/test_simulators_gates_noisy.py rename to source/qdk_package/tests/test_simulators_gates_noisy.py index a880b0e68b..f2769405aa 100644 --- a/source/pip/tests/test_simulators_gates_noisy.py +++ b/source/qdk_package/tests/test_simulators_gates_noisy.py @@ -4,9 +4,11 @@ from collections import Counter import os import pytest -import qsharp -from qsharp import compile, Result, TargetProfile -from qsharp._simulation import run_qir as _run_qir, NoiseConfig, try_create_gpu_adapter +import qdk as qsharp +from qdk._interpreter import compile +from qdk import Result, TargetProfile +from qdk.simulation import run_qir as _run_qir, NoiseConfig +from qdk.simulation._simulation import try_create_gpu_adapter from typing import Literal, List, Optional, TypeAlias diff --git a/source/pip/tests/test_sparse_simulator.py b/source/qdk_package/tests/test_sparse_simulator.py similarity index 98% rename from source/pip/tests/test_sparse_simulator.py rename to source/qdk_package/tests/test_sparse_simulator.py index 83326bf2b1..ec54d45221 100644 --- a/source/pip/tests/test_sparse_simulator.py +++ b/source/qdk_package/tests/test_sparse_simulator.py @@ -9,13 +9,13 @@ import pytest -from qsharp._native import Result +from qdk._native import Result -import qsharp -from qsharp import TargetProfile -from qsharp import openqasm, run +import qdk as qsharp +from qdk import TargetProfile +from qdk import openqasm, run -from qsharp._simulation import NoiseConfig +from qdk.simulation import NoiseConfig current_file_path = Path(__file__) # Get the directory of the current file diff --git a/source/vscode/test/suites/language-service/test-workspace/test-no-lang-metadata.ipynb b/source/vscode/test/suites/language-service/test-workspace/test-no-lang-metadata.ipynb index 440964f30a..6fd9cfd304 100644 --- a/source/vscode/test/suites/language-service/test-workspace/test-no-lang-metadata.ipynb +++ b/source/vscode/test/suites/language-service/test-workspace/test-no-lang-metadata.ipynb @@ -7,7 +7,7 @@ "metadata": {}, "outputs": [], "source": [ - "import qsharp\n" + "from qdk import qsharp\n" ] }, { @@ -46,4 +46,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/source/vscode/test/suites/language-service/test-workspace/test-notebook-profile.ipynb b/source/vscode/test/suites/language-service/test-workspace/test-notebook-profile.ipynb index 652b9b3e64..681a0c244f 100644 --- a/source/vscode/test/suites/language-service/test-workspace/test-notebook-profile.ipynb +++ b/source/vscode/test/suites/language-service/test-workspace/test-notebook-profile.ipynb @@ -7,7 +7,7 @@ "metadata": {}, "outputs": [], "source": [ - "import qsharp\n" + "from qdk import qsharp\n" ] }, { diff --git a/source/vscode/test/suites/language-service/test-workspace/test.ipynb b/source/vscode/test/suites/language-service/test-workspace/test.ipynb index 5a9ca84f31..16fc186f17 100644 --- a/source/vscode/test/suites/language-service/test-workspace/test.ipynb +++ b/source/vscode/test/suites/language-service/test-workspace/test.ipynb @@ -1,88 +1,88 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "1e8e4faa", - "metadata": {}, - "outputs": [], - "source": [ - "import qsharp\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9df62352", - "metadata": { - "vscode": { - "languageId": "qsharp" - } - }, - "outputs": [], - "source": [ - "%%qsharp\n", - "\n", - "operation Test() : Unit {\n", - " let foo = \"hello!\";\n", - " Message(foo);\n", - "}\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1b55e53c", - "metadata": { - "scrolled": false, - "vscode": { - "languageId": "qsharp" - } - }, - "outputs": [], - "source": [ - "%%qsharp\n", - "\n", - "Test()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cc18584a", - "metadata": { - "vscode": { - "languageId": "qsharp" - } - }, - "outputs": [], - "source": [ - "%%qsharp\n", - "\n", - "operation BadSyntax() {\n", - "}\n" - ] + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "1e8e4faa", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk import qsharp\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9df62352", + "metadata": { + "vscode": { + "languageId": "qsharp" + } + }, + "outputs": [], + "source": [ + "%%qsharp\n", + "\n", + "operation Test() : Unit {\n", + " let foo = \"hello!\";\n", + " Message(foo);\n", + "}\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b55e53c", + "metadata": { + "scrolled": false, + "vscode": { + "languageId": "qsharp" } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" + }, + "outputs": [], + "source": [ + "%%qsharp\n", + "\n", + "Test()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc18584a", + "metadata": { + "vscode": { + "languageId": "qsharp" } + }, + "outputs": [], + "source": [ + "%%qsharp\n", + "\n", + "operation BadSyntax() {\n", + "}\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 5 + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 }