diff --git a/build.py b/build.py index c48bcdc8c2..ebfa03a44e 100755 --- a/build.py +++ b/build.py @@ -514,7 +514,7 @@ def run_ci_historic_benchmark(): "ipykernel", "nbconvert", "pandas", - "qiskit>=1.2.0,<1.3.0", + "qiskit>=1.2.0,<2.0.0", ] subprocess.run(pip_install_args, check=True, text=True, cwd=root_dir, env=pip_env) diff --git a/compiler/qsc_qasm3/src/compile.rs b/compiler/qsc_qasm3/src/compile.rs index 74c88e7d5a..07953e58e5 100644 --- a/compiler/qsc_qasm3/src/compile.rs +++ b/compiler/qsc_qasm3/src/compile.rs @@ -134,8 +134,8 @@ impl QasmCompiler { self.prepend_runtime_decls(); let program_ty = self.config.program_ty.clone(); let (package, signature) = match program_ty { - ProgramType::File(name) => self.build_file(name), - ProgramType::Operation(name) => self.build_operation(name), + ProgramType::File => self.build_file(), + ProgramType::Operation => self.build_operation(), ProgramType::Fragments => (self.build_fragments(), None), }; @@ -156,11 +156,12 @@ impl QasmCompiler { /// Build a package with namespace and an operation /// containing the compiled statements. - fn build_file>(&mut self, name: S) -> (Package, Option) { + fn build_file(&mut self) -> (Package, Option) { let tree = self.source.tree(); let whole_span = span_for_syntax_node(tree.syntax()); - let (operation, mut signature) = self.create_entry_operation(name, whole_span); - let ns = "qasm3_import"; + let operation_name = self.config.operation_name(); + let (operation, mut signature) = self.create_entry_operation(operation_name, whole_span); + let ns = self.config.namespace(); signature.ns = Some(ns.to_string()); let top = build_top_level_ns_with_item(whole_span, ns, operation); ( @@ -173,10 +174,11 @@ impl QasmCompiler { } /// Creates an operation with the given name. - fn build_operation>(&mut self, name: S) -> (Package, Option) { + fn build_operation(&mut self) -> (Package, Option) { let tree = self.source.tree(); let whole_span = span_for_syntax_node(tree.syntax()); - let (operation, signature) = self.create_entry_operation(name, whole_span); + let operation_name = self.config.operation_name(); + let (operation, signature) = self.create_entry_operation(operation_name, whole_span); ( Package { nodes: Box::new([TopLevelNode::Stmt(Box::new(ast::Stmt { diff --git a/compiler/qsc_qasm3/src/lib.rs b/compiler/qsc_qasm3/src/lib.rs index dfeb89229d..f4bf212ebb 100644 --- a/compiler/qsc_qasm3/src/lib.rs +++ b/compiler/qsc_qasm3/src/lib.rs @@ -16,7 +16,7 @@ mod types; #[cfg(test)] pub(crate) mod tests; -use std::fmt::Write; +use std::{fmt::Write, sync::Arc}; use miette::Diagnostic; use qsc::Span; @@ -393,6 +393,8 @@ pub struct CompilerConfig { pub qubit_semantics: QubitSemantics, pub output_semantics: OutputSemantics, pub program_ty: ProgramType, + operation_name: Option>, + namespace: Option>, } impl CompilerConfig { @@ -401,13 +403,29 @@ impl CompilerConfig { qubit_semantics: QubitSemantics, output_semantics: OutputSemantics, program_ty: ProgramType, + operation_name: Option>, + namespace: Option>, ) -> Self { Self { qubit_semantics, output_semantics, program_ty, + operation_name, + namespace, } } + + fn operation_name(&self) -> Arc { + self.operation_name + .clone() + .unwrap_or_else(|| Arc::from("program")) + } + + fn namespace(&self) -> Arc { + self.namespace + .clone() + .unwrap_or_else(|| Arc::from("qasm3_import")) + } } impl Default for CompilerConfig { @@ -416,27 +434,27 @@ impl Default for CompilerConfig { qubit_semantics: QubitSemantics::Qiskit, output_semantics: OutputSemantics::Qiskit, program_ty: ProgramType::Fragments, + operation_name: None, + namespace: None, } } } -/// Represents the type of compilation out to create +/// Represents the type of compilation output to create #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProgramType { /// Creates an operation in a namespace as if the program is a standalone - /// file. The param is the name of the operation to create. Input are - /// lifted to the operation params. Output are lifted to the operation - /// return type. The operation is marked as `@EntryPoint` as long as there - /// are no input parameters. - File(String), - /// Creates an operation program is a standalone function. The param is the - /// name of the operation to create. Input are lifted to the operation - /// params. Output are lifted to the operation return type. - Operation(String), + /// file. Inputs are lifted to the operation params. Output are lifted to + /// the operation return type. The operation is marked as `@EntryPoint` + /// as long as there are no input parameters. + File, + /// Programs are compiled to a standalone function. Inputs are lifted to + /// the operation params. Output are lifted to the operation return type. + Operation, /// Creates a list of statements from the program. This is useful for /// interactive environments where the program is a list of statements /// imported into the current scope. - /// This is also useful for testing indifidual statements compilation. + /// This is also useful for testing individual statements compilation. Fragments, } diff --git a/compiler/qsc_qasm3/src/tests.rs b/compiler/qsc_qasm3/src/tests.rs index 8248902f8a..66c8c67e60 100644 --- a/compiler/qsc_qasm3/src/tests.rs +++ b/compiler/qsc_qasm3/src/tests.rs @@ -78,11 +78,13 @@ fn compile_qasm_to_qir(source: &str, profile: Profile) -> Result Q qasm_to_program( source, source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - program_ty: ProgramType::Fragments, - output_semantics: OutputSemantics::OpenQasm, - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::OpenQasm, + ProgramType::Fragments, + None, + None, + ), ) } @@ -164,11 +168,13 @@ pub fn compile_qasm_to_qsharp_file(source: &str) -> miette::Result miette::Result miette::Result<(), Vec> { let unit = crate::qasm_to_program( res.source, res.source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - output_semantics: OutputSemantics::OpenQasm, - program_ty: ProgramType::Fragments, - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::OpenQasm, + ProgramType::Fragments, + None, + None, + ), ); println!("{:?}", unit.errors); assert!(unit.errors.len() == 5); @@ -100,11 +102,13 @@ fn stretch() { let unit = crate::compile::qasm_to_program( res.source, res.source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - output_semantics: OutputSemantics::OpenQasm, - program_ty: ProgramType::Fragments, - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::OpenQasm, + ProgramType::Fragments, + None, + None, + ), ); assert!(unit.has_errors()); println!("{:?}", unit.errors); diff --git a/compiler/qsc_qasm3/src/tests/output.rs b/compiler/qsc_qasm3/src/tests/output.rs index 1cb10cd7ae..db0837571d 100644 --- a/compiler/qsc_qasm3/src/tests/output.rs +++ b/compiler/qsc_qasm3/src/tests/output.rs @@ -37,11 +37,13 @@ fn using_re_semantics_removes_output() -> miette::Result<(), Vec> { let unit = qasm_to_program( res.source, res.source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - output_semantics: OutputSemantics::ResourceEstimation, - program_ty: ProgramType::File("Test".to_string()), - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::ResourceEstimation, + ProgramType::File, + Some("Test".into()), + None, + ), ); fail_on_compilation_errors(&unit); let qsharp = gen_qsharp(&unit.package.expect("no package found")); @@ -90,11 +92,13 @@ fn using_qasm_semantics_captures_all_classical_decls_as_output() -> miette::Resu let unit = qasm_to_program( res.source, res.source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - output_semantics: OutputSemantics::OpenQasm, - program_ty: ProgramType::File("Test".to_string()), - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::OpenQasm, + ProgramType::File, + Some("Test".into()), + None, + ), ); fail_on_compilation_errors(&unit); let qsharp = gen_qsharp(&unit.package.expect("no package found")); @@ -144,11 +148,13 @@ fn using_qiskit_semantics_only_bit_array_is_captured_and_reversed( let unit = qasm_to_program( res.source, res.source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - output_semantics: OutputSemantics::Qiskit, - program_ty: ProgramType::File("Test".to_string()), - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::Qiskit, + ProgramType::File, + Some("Test".into()), + None, + ), ); fail_on_compilation_errors(&unit); let qsharp = gen_qsharp(&unit.package.expect("no package found")); @@ -205,11 +211,13 @@ c2[2] = measure q[4]; let unit = qasm_to_program( res.source, res.source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - output_semantics: OutputSemantics::Qiskit, - program_ty: ProgramType::File("Test".to_string()), - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::Qiskit, + ProgramType::File, + Some("Test".into()), + None, + ), ); fail_on_compilation_errors(&unit); let package = unit.package.expect("no package found"); diff --git a/compiler/qsc_qasm3/src/tests/sample_circuits/bell_pair.rs b/compiler/qsc_qasm3/src/tests/sample_circuits/bell_pair.rs index d16e3685ac..274bb7ab3f 100644 --- a/compiler/qsc_qasm3/src/tests/sample_circuits/bell_pair.rs +++ b/compiler/qsc_qasm3/src/tests/sample_circuits/bell_pair.rs @@ -28,11 +28,13 @@ fn it_compiles() { let unit = qasm_to_program( res.source, res.source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - output_semantics: OutputSemantics::OpenQasm, - program_ty: ProgramType::File("Test".to_string()), - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::OpenQasm, + ProgramType::File, + Some("Test".into()), + None, + ), ); print_compilation_errors(&unit); assert!(!unit.has_errors()); diff --git a/compiler/qsc_qasm3/src/tests/sample_circuits/rgqft_multiplier.rs b/compiler/qsc_qasm3/src/tests/sample_circuits/rgqft_multiplier.rs index b1585934b7..a686856fa8 100644 --- a/compiler/qsc_qasm3/src/tests/sample_circuits/rgqft_multiplier.rs +++ b/compiler/qsc_qasm3/src/tests/sample_circuits/rgqft_multiplier.rs @@ -16,11 +16,13 @@ fn it_compiles() { let unit = qasm_to_program( res.source, res.source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - output_semantics: OutputSemantics::OpenQasm, - program_ty: ProgramType::File("Test".to_string()), - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::OpenQasm, + ProgramType::File, + Some("Test".into()), + None, + ), ); print_compilation_errors(&unit); assert!(!unit.has_errors()); diff --git a/compiler/qsc_qasm3/src/tests/statement/include.rs b/compiler/qsc_qasm3/src/tests/statement/include.rs index 284d917a28..0ab2962ed0 100644 --- a/compiler/qsc_qasm3/src/tests/statement/include.rs +++ b/compiler/qsc_qasm3/src/tests/statement/include.rs @@ -37,11 +37,13 @@ fn programs_with_includes_can_be_parsed() -> miette::Result<(), Vec> { let r = qasm_to_program( res.source, res.source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - output_semantics: OutputSemantics::Qiskit, - program_ty: ProgramType::File("Test".to_string()), - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::Qiskit, + ProgramType::File, + Some("Test".into()), + None, + ), ); let qsharp = qsharp_from_qasm_compilation(r)?; expect![ diff --git a/compiler/qsc_qasm3/src/tests/statement/reset.rs b/compiler/qsc_qasm3/src/tests/statement/reset.rs index 6c03040c21..b7506d0f1c 100644 --- a/compiler/qsc_qasm3/src/tests/statement/reset.rs +++ b/compiler/qsc_qasm3/src/tests/statement/reset.rs @@ -31,11 +31,13 @@ fn reset_calls_are_generated_from_qasm() -> miette::Result<(), Vec> { let unit = qasm_to_program( res.source, res.source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - output_semantics: OutputSemantics::Qiskit, - program_ty: ProgramType::File("Test".to_string()), - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + OutputSemantics::Qiskit, + ProgramType::File, + Some("Test".into()), + None, + ), ); fail_on_compilation_errors(&unit); let qsharp = gen_qsharp(&unit.package.expect("no package found")); diff --git a/pip/pyproject.toml b/pip/pyproject.toml index eaf1f91d01..f065e366fe 100644 --- a/pip/pyproject.toml +++ b/pip/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ [project.optional-dependencies] jupyterlab = ["qsharp-jupyterlab"] widgets = ["qsharp-widgets"] -qiskit = ["qiskit>=1.2.0,<1.3.0"] +qiskit = ["qiskit>=1.2.0,<2.0.0"] [build-system] requires = ["maturin ~= 1.2.0"] diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi index 3f6e679a31..3a2aa32dd9 100644 --- a/pip/qsharp/_native.pyi +++ b/pip/qsharp/_native.pyi @@ -8,6 +8,62 @@ from typing import Any, Callable, Optional, Dict, List, Tuple # E302 is fighting with the formatter for number of blank lines # flake8: noqa: E302 +class OutputSemantics(Enum): + """ + Represents the output semantics for OpenQASM 3 compilation. + Each has implications on the output of the compilation + and the semantic checks that are performed. + """ + + Qiskit: OutputSemantics + """ + The output is in Qiskit format meaning that the output + is all of the classical registers, in reverse order + in which they were added to the circuit with each + bit within each register in reverse order. + """ + + OpenQasm: OutputSemantics + """ + [OpenQASM 3 has two output modes](https://openqasm.com/language/directives.html#input-output) + - If the programmer provides one or more `output` declarations, then + variables described as outputs will be returned as output. + The spec make no mention of endianness or order of the output. + - Otherwise, assume all of the declared variables are returned as output. + """ + + ResourceEstimation: OutputSemantics + """ + No output semantics are applied. The entry point returns `Unit`. + """ + +class ProgramType(Enum): + """ + Represents the type of compilation output to create + """ + + File: ProgramType + """ + Creates an operation in a namespace as if the program is a standalone + file. Inputs are lifted to the operation params. Output are lifted to + the operation return type. The operation is marked as `@EntryPoint` + as long as there are no input parameters. + """ + + Operation: ProgramType + """ + Programs are compiled to a standalone function. Inputs are lifted to + the operation params. Output are lifted to the operation return type. + """ + + Fragments: ProgramType + """ + Creates a list of statements from the program. This is useful for + interactive environments where the program is a list of statements + imported into the current scope. + This is also useful for testing individual statements compilation. + """ + class TargetProfile(Enum): """ A Q# target profile. diff --git a/pip/qsharp/interop/qiskit/__init__.py b/pip/qsharp/interop/qiskit/__init__.py index 1ae4001c6e..5cf469efe4 100644 --- a/pip/qsharp/interop/qiskit/__init__.py +++ b/pip/qsharp/interop/qiskit/__init__.py @@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional, Union from ...estimator import EstimatorParams, EstimatorResult -from ..._native import QasmError, TargetProfile +from ..._native import OutputSemantics, ProgramType, QasmError from .backends import QSharpBackend, ResourceEstimatorBackend, QirTarget from .jobs import QsJob, QsSimJob, ReJob, QsJobSet from .execution import DetaultExecutor diff --git a/pip/qsharp/interop/qiskit/backends/backend_base.py b/pip/qsharp/interop/qiskit/backends/backend_base.py index 680cdbc627..d7da53b608 100644 --- a/pip/qsharp/interop/qiskit/backends/backend_base.py +++ b/pip/qsharp/interop/qiskit/backends/backend_base.py @@ -434,12 +434,12 @@ def _qsharp(self, circuit: QuantumCircuit, **kwargs) -> str: Args: circuit (QuantumCircuit): The QuantumCircuit to be executed. - **options: Additional options for the execution. + **options: Additional options for the execution. Defaults to backend config values. - Any options for the transpiler, exporter, or Qiskit passes configuration. Defaults to backend config values. Common values include: 'optimization_level', 'basis_gates', 'includes', 'search_path'. - + - output_semantics (OutputSemantics, optional): The output semantics for the compilation. Returns: str: The converted QASM3 code as a string. Any supplied includes are emitted as include statements at the top of the program. @@ -452,9 +452,17 @@ def _qsharp(self, circuit: QuantumCircuit, **kwargs) -> str: args = { "name": kwargs.get("name", circuit.name), - "search_path": kwargs.get("search_path", "."), } - qsharp_source = self._qsharp(qasm3_source, **args) + + if search_path := kwargs.pop("search_path", "."): + args["search_path"] = search_path + + if output_semantics := kwargs.pop( + "output_semantics", self.options.get("output_semantics", default=None) + ): + args["output_semantics"] = output_semantics + + qsharp_source = self._qasm3_to_qsharp(qasm3_source, **args) return qsharp_source def qir( @@ -470,8 +478,8 @@ def qir( **kwargs: Additional options for the execution. - params (str, optional): The entry expression for the QIR conversion. Defaults to None. - target_profile (TargetProfile, optional): The target profile for the backend. Defaults to backend config value. + - output_semantics (OutputSemantics, optional): The output semantics for the compilation. Defaults to backend config value. - search_path (str, optional): The search path for the backend. Defaults to '.'. - Returns: str: The converted QIR code as a string. @@ -486,18 +494,25 @@ def qir( qasm3_source = self._qasm3(circuit, **kwargs) - qir_args = { + args = { "name": name, "target_profile": target_profile, - "search_path": kwargs.pop("search_path", "."), } - params = kwargs.pop("params", None) - if params is not None: - qir_args["params"] = params - return self._qir(qasm3_source, **qir_args) + if search_path := kwargs.pop("search_path", "."): + args["search_path"] = search_path + + if params := kwargs.pop("params", None): + args["params"] = params + + if output_semantics := kwargs.pop( + "output_semantics", self.options.get("output_semantics", default=None) + ): + args["output_semantics"] = output_semantics + + return self._qasm3_to_qir(qasm3_source, **args) - def _qir( + def _qasm3_to_qir( self, source: str, **kwargs, @@ -515,7 +530,7 @@ def _qir( **kwargs, ) - def _qsharp( + def _qasm3_to_qsharp( self, source: str, **kwargs, diff --git a/pip/qsharp/interop/qiskit/backends/qsharp_backend.py b/pip/qsharp/interop/qiskit/backends/qsharp_backend.py index a424bb6203..3a47d9b1cb 100644 --- a/pip/qsharp/interop/qiskit/backends/qsharp_backend.py +++ b/pip/qsharp/interop/qiskit/backends/qsharp_backend.py @@ -11,6 +11,7 @@ from qiskit.providers import Options from qiskit.transpiler.target import Target from .... import Result, TargetProfile +from .. import OutputSemantics from ..execution import DetaultExecutor from ..jobs import QsSimJob, QsJobSet from .backend_base import BackendBase @@ -65,11 +66,12 @@ def __init__( - name (str): The name of the circuit. This is used as the entry point for the program. The circuit name will be used if not specified. - target_profile (TargetProfile): The target profile to use for the compilation. - - shots (int): The number of shots to run the program for. Defaults to 1024. - - seed (int): The seed to use for the random number generator. Defaults to None. + - output_semantics (OutputSemantics, optional): The output semantics for the compilation. Defaults to `Qiskit`. + - shots (int): The number of shots to run the program for. Defaults to `1024`. + - seed (int): The seed to use for the random number generator. Defaults to `None`. - search_path (str): The path to search for imports. Defaults to '.'. - output_fn (Callable[[Output], None]): A callback function to - receive the output of the circuit. Defaults to None. + receive the output of the circuit. Defaults to `None`. - executor(ThreadPoolExecutor or other Executor): The executor to be used to submit the job. Defaults to SynchronousExecutor. """ @@ -93,6 +95,7 @@ def _default_options(cls): seed=None, output_fn=None, target_profile=TargetProfile.Unrestricted, + output_semantics=OutputSemantics.Qiskit, executor=DetaultExecutor(), ) @@ -106,11 +109,12 @@ def run( Args: run_input (QuantumCircuit): The QuantumCircuit to be executed. - **options: Additional options for the execution. + **options: Additional options for the execution. Defaults to backend config values. - name (str): The name of the circuit. This is used as the entry point for the program. The circuit name will be used if not specified. - params (Optional[str]): The entry expression to use for the program. Defaults to None. - target_profile (TargetProfile): The target profile to use for the compilation. + - output_semantics (OutputSemantics, optional): The output semantics for the compilation. - shots (int): The number of shots to run the program for. Defaults to 1024. - seed (int): The seed to use for the random number generator. Defaults to None. - search_path (str): The path to search for imports. Defaults to '.'. @@ -213,8 +217,9 @@ def _run_qasm3( Parameters: source (str): The input OpenQASM 3 string to be processed. - **options: Additional keyword arguments to pass to the execution. - - target_profile (TargetProfile): The target profile to use for execution. + **options: Additional keyword arguments to pass to the execution. Defaults to backend config values. + - target_profile (TargetProfile): The target profile to use for the compilation. + - output_semantics (OutputSemantics, optional): The output semantics for the compilation. - name (str): The name of the circuit. This is used as the entry point for the program. Defaults to 'program'. - search_path (str): The optional search path for resolving qasm3 imports. - shots (int): The number of shots to run the program for. Defaults to 1. @@ -237,24 +242,25 @@ def callback(output: Output) -> None: output_fn = options.pop("output_fn", callback) - name = options.pop("name", default_options["name"]) - target_profile = options.pop("target_profile", default_options["target_profile"]) - search_path = options.pop("search_path", default_options["search_path"]) - shots = options.pop("shots", default_options["shots"]) - seed = options.pop("seed", default_options["seed"]) + def value_or_default(key: str) -> Any: + return options.pop(key, default_options[key]) # when passing the args into the rust layer, any kwargs with None values # will cause an error, so we need to filter them out. args = {} - if name is not None: + if name := value_or_default("name"): args["name"] = name - if target_profile is not None: + + if target_profile := value_or_default("target_profile"): args["target_profile"] = target_profile - if search_path is not None: + if output_semantics := value_or_default("output_semantics"): + args["output_semantics"] = output_semantics + + if search_path := value_or_default("search_path"): args["search_path"] = search_path - if shots is not None: + if shots := value_or_default("shots"): args["shots"] = shots - if seed is not None: + if seed := value_or_default("seed"): args["seed"] = seed return run_qasm3( diff --git a/pip/qsharp/interop/qiskit/backends/re_backend.py b/pip/qsharp/interop/qiskit/backends/re_backend.py index 8aa397a208..54d90d6892 100644 --- a/pip/qsharp/interop/qiskit/backends/re_backend.py +++ b/pip/qsharp/interop/qiskit/backends/re_backend.py @@ -14,6 +14,7 @@ from .compilation import Compilation from .errors import Errors from .backend_base import BackendBase +from .. import OutputSemantics from ..jobs import ReJob from ..execution import DetaultExecutor from ...._fs import read_file, list_directory, resolve @@ -56,7 +57,6 @@ def __init__( - name (str): The name of the circuit. This is used as the entry point for the program. The circuit name will be used if not specified. - search_path (str): Path to search in for qasm3 imports. Defaults to '.'. - - target_profile (TargetProfile): The target profile to use for the backend. - executor(ThreadPoolExecutor or other Executor): The executor to be used to submit the job. Defaults to SynchronousExecutor. """ @@ -84,6 +84,7 @@ def _default_options(cls): name="program", search_path=".", target_profile=TargetProfile.Unrestricted, + output_semantics=OutputSemantics.ResourceEstimation, executor=DetaultExecutor(), ) diff --git a/pip/src/interop.rs b/pip/src/interop.rs index ffcee5fbaa..7c961a6d6a 100644 --- a/pip/src/interop.rs +++ b/pip/src/interop.rs @@ -18,14 +18,13 @@ use qsc::{ use qsc::{Backend, PackageType, SparseSim}; use qsc_qasm3::io::SourceResolver; use qsc_qasm3::{ - qasm_to_program, CompilerConfig, OperationSignature, OutputSemantics, ProgramType, - QasmCompileUnit, QubitSemantics, + qasm_to_program, CompilerConfig, OperationSignature, QasmCompileUnit, QubitSemantics, }; use crate::fs::file_system; use crate::interpreter::{ - format_error, format_errors, OptionalCallbackReceiver, QSharpError, QasmError, TargetProfile, - ValueWrapper, + format_error, format_errors, OptionalCallbackReceiver, OutputSemantics, ProgramType, + QSharpError, QasmError, TargetProfile, ValueWrapper, }; use resource_estimator as re; @@ -92,7 +91,7 @@ pub fn run_qasm3( let kwargs = kwargs.unwrap_or_else(|| PyDict::new_bound(py)); let target = get_target_profile(&kwargs)?; - let name = get_name(&kwargs)?; + let operation_name = get_operation_name(&kwargs)?; let seed = get_seed(&kwargs); let shots = get_shots(&kwargs)?; let search_path = get_search_path(&kwargs)?; @@ -102,9 +101,9 @@ pub fn run_qasm3( let (package, source_map, signature) = compile_qasm_enriching_errors( source, - &name, + &operation_name, &resolver, - ProgramType::File(name.to_string()), + ProgramType::File, OutputSemantics::Qiskit, false, )?; @@ -169,21 +168,24 @@ pub(crate) fn resource_estimate_qasm3( ) -> PyResult { let kwargs = kwargs.unwrap_or_else(|| PyDict::new_bound(py)); - let name = get_name(&kwargs)?; + let operation_name = get_operation_name(&kwargs)?; let search_path = get_search_path(&kwargs)?; let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); let resolver = ImportResolver::new(fs, PathBuf::from(search_path)); - let program_type = ProgramType::File(name.to_string()); + let program_type = ProgramType::File; let output_semantics = OutputSemantics::ResourceEstimation; - let unit = compile_qasm(source, &name, &resolver, program_type, output_semantics)?; - let (source_map, _, package, _) = unit.into_tuple(); - match crate::interop::estimate_qasm3( - package.expect("Package must exist when there are no errors"), - source_map, - job_params, - ) { + let (package, source_map, _) = compile_qasm_enriching_errors( + source, + &operation_name, + &resolver, + program_type, + output_semantics, + false, + )?; + + match crate::interop::estimate_qasm3(package, source_map, job_params) { Ok(estimate) => Ok(estimate), Err(errors) if matches!(errors[0], re::Error::Interpreter(_)) => { Err(QSharpError::new_err(format_errors( @@ -229,19 +231,20 @@ pub(crate) fn compile_qasm3_to_qir( let kwargs = kwargs.unwrap_or_else(|| PyDict::new_bound(py)); let target = get_target_profile(&kwargs)?; - let name = get_name(&kwargs)?; + let operation_name = get_operation_name(&kwargs)?; let search_path = get_search_path(&kwargs)?; let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); let resolver = ImportResolver::new(fs, PathBuf::from(search_path)); - let program_type = ProgramType::File(name.to_string()); + let program_ty = get_program_type(&kwargs)?; + let output_semantics = get_output_semantics(&kwargs)?; let (package, source_map, signature) = compile_qasm_enriching_errors( source, - &name, + &operation_name, &resolver, - program_type, - OutputSemantics::Qiskit, + program_ty, + output_semantics, false, )?; @@ -257,20 +260,22 @@ pub(crate) fn compile_qasm3_to_qir( pub(crate) fn compile_qasm, R: SourceResolver>( source: S, - name: S, + operation_name: S, resolver: &R, - program_type: ProgramType, + program_ty: ProgramType, output_semantics: OutputSemantics, ) -> PyResult { - let parse_result = - qsc_qasm3::parse::parse_source(source, format!("{}.qasm", name.as_ref()), resolver) - .map_err(|report| { - // this will only fail if a file cannot be read - // most likely due to a missing file or search path - QasmError::new_err(format!("{report:?}")) - })?; - - // + let parse_result = qsc_qasm3::parse::parse_source( + source, + format!("{}.qasm", operation_name.as_ref()), + resolver, + ) + .map_err(|report| { + // this will only fail if a file cannot be read + // most likely due to a missing file or search path + QasmError::new_err(format!("{report:?}")) + })?; + if parse_result.has_errors() { return Err(QasmError::new_err(format_qasm_errors( parse_result.errors(), @@ -279,11 +284,13 @@ pub(crate) fn compile_qasm, R: SourceResolver>( let unit = qasm_to_program( parse_result.source, parse_result.source_map, - CompilerConfig { - qubit_semantics: QubitSemantics::Qiskit, - output_semantics, - program_ty: program_type, - }, + CompilerConfig::new( + QubitSemantics::Qiskit, + output_semantics.into(), + program_ty.into(), + Some(operation_name.as_ref().into()), + None, + ), ); if unit.has_errors() { @@ -294,13 +301,19 @@ pub(crate) fn compile_qasm, R: SourceResolver>( pub(crate) fn compile_qasm_enriching_errors, R: SourceResolver>( source: S, - name: S, + operation_name: S, resolver: &R, - program_type: ProgramType, + program_ty: ProgramType, output_semantics: OutputSemantics, allow_input_params: bool, ) -> PyResult<(Package, SourceMap, OperationSignature)> { - let unit = compile_qasm(source, name, resolver, program_type, output_semantics)?; + let unit = compile_qasm( + source, + operation_name, + resolver, + program_ty, + output_semantics, + )?; if unit.has_errors() { return Err(QasmError::new_err(format_qasm_errors(unit.errors()))); @@ -356,19 +369,20 @@ pub(crate) fn compile_qasm3_to_qsharp( ) -> PyResult { let kwargs = kwargs.unwrap_or_else(|| PyDict::new_bound(py)); - let name = get_name(&kwargs)?; + let operation_name = get_operation_name(&kwargs)?; let search_path = get_search_path(&kwargs)?; let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); let resolver = ImportResolver::new(fs, PathBuf::from(search_path)); - let program_type = ProgramType::File(name.to_string()); + let program_ty = get_program_type(&kwargs)?; + let output_semantics = get_output_semantics(&kwargs)?; let (package, _, _) = compile_qasm_enriching_errors( source, - &name, + &operation_name, &resolver, - program_type, - OutputSemantics::Qiskit, + program_ty, + output_semantics, true, )?; @@ -598,22 +612,27 @@ pub(crate) fn get_search_path(kwargs: &Bound<'_, PyDict>) -> PyResult { ) } -/// Extracts the run type from the kwargs dictionary. -/// If the run type is not present, returns an error. -/// Otherwise, returns the run type as a string. -/// -/// Note: This should become an enum in the future. -pub(crate) fn get_run_type(kwargs: &Bound<'_, PyDict>) -> PyResult { - kwargs.get_item("run_type")?.map_or_else( - || Err(PyException::new_err("Could not parse run type".to_string())), - |x| x.extract::(), - ) +/// Extracts the program type from the kwargs dictionary. +pub(crate) fn get_program_type(kwargs: &Bound<'_, PyDict>) -> PyResult { + let target = kwargs + .get_item("program_ty")? + .map_or_else(|| Ok(ProgramType::File), |x| x.extract::())?; + Ok(target) +} + +/// Extracts the output semantics from the kwargs dictionary. +pub(crate) fn get_output_semantics(kwargs: &Bound<'_, PyDict>) -> PyResult { + let target = kwargs.get_item("output_semantics")?.map_or_else( + || Ok(OutputSemantics::Qiskit), + |x| x.extract::(), + )?; + Ok(target) } /// Extracts the name from the kwargs dictionary. /// If the name is not present, returns "program". /// Otherwise, returns the name after sanitizing it. -pub(crate) fn get_name(kwargs: &Bound<'_, PyDict>) -> PyResult { +pub(crate) fn get_operation_name(kwargs: &Bound<'_, PyDict>) -> PyResult { let name = kwargs .get_item("name")? .map_or_else(|| Ok("program".to_string()), |x| x.extract::())?; diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index bf8deb64c0..b4bd7387f0 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -31,7 +31,7 @@ use qsc::{ target::Profile, LanguageFeatures, PackageType, SourceMap, }; -use qsc_qasm3::{OutputSemantics, ProgramType}; + use resource_estimator::{self as re, estimate_expr}; use std::{cell::RefCell, fmt::Write, path::PathBuf, rc::Rc}; @@ -51,6 +51,8 @@ use std::{cell::RefCell, fmt::Write, path::PathBuf, rc::Rc}; /// corresponding exception. fn verify_classes_are_sendable() { fn is_send() {} + is_send::(); + is_send::(); is_send::(); is_send::(); is_send::(); @@ -62,6 +64,8 @@ fn verify_classes_are_sendable() { #[pymodule] fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> { verify_classes_are_sendable(); + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; @@ -116,6 +120,70 @@ impl From for Profile { } } +// This ordering must match the _native.pyi file. +#[derive(Clone, Copy, PartialEq)] +#[pyclass(eq, eq_int)] +#[allow(non_camel_case_types)] +/// Represents the output semantics for OpenQASM 3 compilation. +/// Each has implications on the output of the compilation +/// and the semantic checks that are performed. +pub(crate) enum OutputSemantics { + /// The output is in Qiskit format meaning that the output + /// is all of the classical registers, in reverse order + /// in which they were added to the circuit with each + /// bit within each register in reverse order. + Qiskit, + /// [OpenQASM 3 has two output modes](https://openqasm.com/language/directives.html#input-output) + /// - If the programmer provides one or more `output` declarations, then + /// variables described as outputs will be returned as output. + /// The spec make no mention of endianness or order of the output. + /// - Otherwise, assume all of the declared variables are returned as output. + OpenQasm, + /// No output semantics are applied. The entry point returns `Unit`. + ResourceEstimation, +} + +impl From for qsc_qasm3::OutputSemantics { + fn from(output_semantics: OutputSemantics) -> Self { + match output_semantics { + OutputSemantics::Qiskit => qsc_qasm3::OutputSemantics::Qiskit, + OutputSemantics::OpenQasm => qsc_qasm3::OutputSemantics::OpenQasm, + OutputSemantics::ResourceEstimation => qsc_qasm3::OutputSemantics::ResourceEstimation, + } + } +} + +// This ordering must match the _native.pyi file. +#[derive(Clone, PartialEq)] +#[pyclass(eq)] +#[allow(non_camel_case_types)] +/// Represents the type of compilation output to create +pub enum ProgramType { + /// Creates an operation in a namespace as if the program is a standalone + /// file. Inputs are lifted to the operation params. Output are lifted to + /// the operation return type. The operation is marked as `@EntryPoint` + /// as long as there are no input parameters. + File, + /// Programs are compiled to a standalone function. Inputs are lifted to + /// the operation params. Output are lifted to the operation return type. + Operation, + /// Creates a list of statements from the program. This is useful for + /// interactive environments where the program is a list of statements + /// imported into the current scope. + /// This is also useful for testing individual statements compilation. + Fragments, +} + +impl From for qsc_qasm3::ProgramType { + fn from(output_semantics: ProgramType) -> Self { + match output_semantics { + ProgramType::File => qsc_qasm3::ProgramType::File, + ProgramType::Operation => qsc_qasm3::ProgramType::Operation, + ProgramType::Fragments => qsc_qasm3::ProgramType::Fragments, + } + } +} + #[pyclass(unsendable)] pub(crate) struct Interpreter { pub(crate) interpreter: interpret::Interpreter, @@ -336,11 +404,12 @@ impl Interpreter { let kwargs = kwargs.unwrap_or_else(|| PyDict::new_bound(py)); - let name = crate::interop::get_name(&kwargs)?; + let operation_name = crate::interop::get_operation_name(&kwargs)?; let seed = crate::interop::get_seed(&kwargs); let shots = crate::interop::get_shots(&kwargs)?; let search_path = crate::interop::get_search_path(&kwargs)?; - let run_type = crate::interop::get_run_type(&kwargs)?; + let program_type = crate::interop::get_program_type(&kwargs)?; + let output_semantics = crate::interop::get_output_semantics(&kwargs)?; let fs = crate::interop::create_filesystem_from_py( py, @@ -350,17 +419,13 @@ impl Interpreter { fetch_github, ); let resolver = ImportResolver::new(fs, PathBuf::from(search_path)); - let program_type = match run_type.as_str() { - "statements" => ProgramType::Fragments, - "operation" => ProgramType::Operation(name.to_string()), - _ => ProgramType::File(name.to_string()), - }; + let (package, _source_map, signature) = compile_qasm_enriching_errors( source, - &name, + &operation_name, &resolver, program_type.clone(), - OutputSemantics::Qiskit, + output_semantics, false, )?; @@ -370,7 +435,7 @@ impl Interpreter { .map_err(|errors| QSharpError::new_err(format_errors(errors)))?; match program_type { - ProgramType::File(..) => { + ProgramType::File => { let entry_expr = signature.create_entry_expr_from_params(String::new()); self.interpreter .set_entry_expr(&entry_expr) diff --git a/pip/tests-integration/interop_qiskit/test_qir.py b/pip/tests-integration/interop_qiskit/test_qir.py index 7b4b99d95d..f9cd14375a 100644 --- a/pip/tests-integration/interop_qiskit/test_qir.py +++ b/pip/tests-integration/interop_qiskit/test_qir.py @@ -14,10 +14,27 @@ from .test_circuits import ( generate_repro_information, ) - from qsharp.interop.qiskit import QasmError, QirTarget + from qsharp.interop.qiskit import ( + OutputSemantics, + QSharpBackend, + QasmError, + QirTarget, + ) from qiskit.circuit import QuantumCircuit, Parameter, Gate from qiskit.circuit.quantumcircuit import QubitSpecifier - from qsharp.interop.qiskit.backends import QSharpBackend + + +def get_resource_path(file_name: Optional[str] = None) -> str: + current_directory = os.path.dirname(os.path.abspath(__file__)) + if file_name is None: + return os.path.join(current_directory, "resources") + return os.path.join(current_directory, "resources", file_name) + + +def read_resource_file(file_name: str) -> str: + resource_path = get_resource_path(file_name) + with open(resource_path, encoding="utf-8") as f: + return f.read() @pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) @@ -126,19 +143,6 @@ def test_generating_qir_without_registers_raises(): raise RuntimeError(additional_info) from ex -def get_resource_path(file_name: Optional[str] = None) -> str: - current_directory = os.path.dirname(os.path.abspath(__file__)) - if file_name is None: - return os.path.join(current_directory, "resources") - return os.path.join(current_directory, "resources", file_name) - - -def read_resource_file(file_name: str) -> str: - resource_path = get_resource_path(file_name) - with open(resource_path, encoding="utf-8") as f: - return f.read() - - @pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) @ignore_on_failure def test_custom_qir_intrinsics_generates_qir(): @@ -203,3 +207,30 @@ def __init__(self): backend = QSharpBackend(target_profile=target_profile, target=target) result = backend.run(circuit, **options).result() assert result.get_counts() == {"1": 1024} + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qir_smoke() -> None: + circuit = QuantumCircuit(2, 2) + circuit.x(0) + circuit.cx(0, 1) + circuit.measure_all(add_bits=False) + backend = QSharpBackend(target_profile=TargetProfile.Base) + res = backend.qir(circuit) + assert res is not None + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qir_re_output_single_unit_tuple() -> None: + circuit = QuantumCircuit(2, 2) + circuit.x(0) + circuit.cx(0, 1) + circuit.measure_all(add_bits=False) + + backend = QSharpBackend(target_profile=TargetProfile.Adaptive_RI) + output_semantics = OutputSemantics.ResourceEstimation + + res = backend.qir(circuit, output_semantics=output_semantics) + assert res is not None + call = "call void @__quantum__rt__tuple_record_output(i64 0, i8* null)" + assert call in res diff --git a/pip/tests-integration/interop_qiskit/test_qsharp.py b/pip/tests-integration/interop_qiskit/test_qsharp.py new file mode 100644 index 0000000000..a92bdb0385 --- /dev/null +++ b/pip/tests-integration/interop_qiskit/test_qsharp.py @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import pytest + +from . import QISKIT_AVAILABLE, SKIP_REASON + + +if QISKIT_AVAILABLE: + from qsharp.interop.qiskit import ( + OutputSemantics, + ProgramType, + QSharpBackend, + ) + from qiskit.circuit import QuantumCircuit + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qsharp_smoke() -> None: + circuit = QuantumCircuit(2, 2) + circuit.x(0) + circuit.cx(0, 1) + circuit.measure_all(add_bits=False) + circuit.name = "smoke" + + backend = QSharpBackend() + res = backend._qsharp(circuit) + assert res is not None + assert "qasm3_import" in res + assert "operation smoke() : Result[]" in res + assert "Microsoft.Quantum.Arrays.Reversed" in res + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qsharp_disable_output() -> None: + circuit = QuantumCircuit(2, 2) + circuit.x(0) + circuit.cx(0, 1) + circuit.measure_all(add_bits=False) + circuit.name = "circuit_with_unit_output" + backend = QSharpBackend() + output_semantics = OutputSemantics.ResourceEstimation + + res = backend._qsharp(circuit, output_semantics=output_semantics) + assert "operation circuit_with_unit_output() : Unit" in res + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qsharp_openqasm_output_semantics() -> None: + circuit = QuantumCircuit(2, 2) + circuit.x(0) + circuit.cx(0, 1) + circuit.measure_all(add_bits=False) + circuit.name = "circuit_with_unit_output" + backend = QSharpBackend() + output_semantics = OutputSemantics.OpenQasm + + res = backend._qsharp(circuit, output_semantics=output_semantics) + assert "Microsoft.Quantum.Arrays.Reversed" not in res diff --git a/pip/tests-integration/interop_qiskit/test_re.py b/pip/tests-integration/interop_qiskit/test_re.py index 2380fd87d7..01777ca841 100644 --- a/pip/tests-integration/interop_qiskit/test_re.py +++ b/pip/tests-integration/interop_qiskit/test_re.py @@ -4,6 +4,7 @@ from concurrent.futures import ThreadPoolExecutor import pytest +from qsharp import QSharpError from qsharp.estimator import ( EstimatorParams, QubitParams, @@ -14,7 +15,10 @@ from interop_qiskit import QISKIT_AVAILABLE, SKIP_REASON if QISKIT_AVAILABLE: - from qiskit import QuantumCircuit + from .test_circuits import ( + generate_repro_information, + ) + from qiskit.circuit import QuantumCircuit, Parameter from qiskit.circuit.library import RGQFTMultiplier from qsharp.interop.qiskit import ResourceEstimatorBackend @@ -97,3 +101,26 @@ def test_estimate_qiskit_rgqft_multiplier_in_threadpool() -> None: "measurementCount": 0, } ) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_estimating_with_unbound_param_raises(): + theta = Parameter("theta") + + circuit = QuantumCircuit(1) + circuit.name = "test" + circuit.rx(theta, 0) + circuit.measure_all() + + backend = ResourceEstimatorBackend() + try: + with pytest.raises(QSharpError) as ex: + _ = backend.run(circuit).result() + message = str(ex.value) + assert "Circuit has unbound input parameters" in message + assert "help: Parameters: theta: Double" in message + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex diff --git a/pip/tests-integration/interop_qiskit/test_run_sim.py b/pip/tests-integration/interop_qiskit/test_run_sim.py index f85185260f..ad3c8c475c 100644 --- a/pip/tests-integration/interop_qiskit/test_run_sim.py +++ b/pip/tests-integration/interop_qiskit/test_run_sim.py @@ -3,12 +3,15 @@ from concurrent.futures import ThreadPoolExecutor import pytest -from qsharp import TargetProfile +from qsharp import QSharpError, TargetProfile from interop_qiskit import QISKIT_AVAILABLE, SKIP_REASON if QISKIT_AVAILABLE: - from qiskit import QuantumCircuit + from .test_circuits import ( + generate_repro_information, + ) + from qiskit.circuit import QuantumCircuit, Parameter from qiskit_aer import AerSimulator from qiskit.qasm3 import loads as from_qasm3 from qiskit.providers import JobStatus @@ -135,3 +138,26 @@ def test_get_counts_matches_qiskit_simulator_multiple_circuits(): except Exception as ex: additional_info = generate_repro_information(circuit, backend) raise RuntimeError(additional_info) from ex + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_simulting_with_unbound_param_raises(): + theta = Parameter("theta") + + circuit = QuantumCircuit(1) + circuit.name = "test" + circuit.rx(theta, 0) + circuit.measure_all() + + backend = QSharpBackend() + try: + with pytest.raises(QSharpError) as ex: + _ = backend.run(circuit).result() + message = str(ex.value) + assert "Circuit has unbound input parameters" in message + assert "help: Parameters: theta: Double" in message + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex