microsoft/qdk

Public

mirrored fromhttps://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.18.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/pip/src/interpreter.rs

1262lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use crate::{
5 displayable_output::{DisplayableMatrix, DisplayableOutput, DisplayableState},
6 fs::file_system,
7 generic_estimator::register_generic_estimator_submodule,
8 interop::{
9 circuit_qasm_program, compile_qasm_program_to_qir, compile_qasm_to_qsharp,
10 create_filesystem_from_py, get_operation_name, get_output_semantics, get_program_type,
11 get_search_path, resource_estimate_qasm_program, run_qasm_program,
12 },
13 noisy_simulator::register_noisy_simulator_submodule,
14};
15use miette::{Diagnostic, Report};
16use num_bigint::{BigInt, BigUint};
17use num_complex::Complex64;
18use pyo3::{
19 create_exception,
20 exceptions::{PyException, PyValueError},
21 prelude::*,
22 types::{PyDict, PyList, PyString, PyTuple, PyType},
23 IntoPyObjectExt,
24};
25use qsc::{
26 error::WithSource,
27 fir::{self},
28 hir::ty::{Prim, Ty},
29 interpret::{
30 self,
31 output::{Error, Receiver},
32 CircuitEntryPoint, PauliNoise, Value,
33 },
34 packages::BuildableProgram,
35 project::{FileSystem, PackageCache, PackageGraphSources, ProjectType},
36 qasm::{compiler::compile_to_qsharp_ast_with_config, CompilerConfig, QubitSemantics},
37 target::Profile,
38 LanguageFeatures, PackageType, SourceMap,
39};
40
41use resource_estimator::{self as re, estimate_call, estimate_expr};
42use std::{cell::RefCell, fmt::Write, path::PathBuf, rc::Rc, str::FromStr, sync::Arc};
43
44/// If the classes are not Send, the Python interpreter
45/// will not be able to use them in a separate thread.
46///
47/// This function is used to verify that the classes are Send.
48/// The code will fail to compile if the classes are not Send.
49///
50/// ### Note
51/// `QSharpError`, and `QasmError` are not `Send`, *BUT*
52/// we return `QasmError::new_err` or `QSharpError::new_err` which
53/// actually returns a `PyErr` that is `Send` and the args passed
54/// into the `new_err` call must also impl `Send`.
55/// Because of this, we don't need to check the `Send`-ness of
56/// them. On the Python side, the `PyErr` is converted into the
57/// corresponding exception.
58fn verify_classes_are_sendable() {
59 fn is_send<T: Send>() {}
60 is_send::<OutputSemantics>();
61 is_send::<ProgramType>();
62 is_send::<TargetProfile>();
63 is_send::<Result>();
64 is_send::<Pauli>();
65 is_send::<Output>();
66 is_send::<StateDumpData>();
67 is_send::<Circuit>();
68}
69
70#[pymodule]
71fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> {
72 verify_classes_are_sendable();
73 m.add_class::<OutputSemantics>()?;
74 m.add_class::<ProgramType>()?;
75 m.add_class::<TargetProfile>()?;
76 m.add_class::<Interpreter>()?;
77 m.add_class::<Result>()?;
78 m.add_class::<Pauli>()?;
79 m.add_class::<Output>()?;
80 m.add_class::<StateDumpData>()?;
81 m.add_class::<Circuit>()?;
82 m.add_class::<GlobalCallable>()?;
83 m.add_function(wrap_pyfunction!(physical_estimates, m)?)?;
84 m.add("QSharpError", py.get_type::<QSharpError>())?;
85 register_noisy_simulator_submodule(py, m)?;
86 register_generic_estimator_submodule(m)?;
87 // QASM interop
88 m.add("QasmError", py.get_type::<QasmError>())?;
89 m.add_function(wrap_pyfunction!(resource_estimate_qasm_program, m)?)?;
90 m.add_function(wrap_pyfunction!(run_qasm_program, m)?)?;
91 m.add_function(wrap_pyfunction!(circuit_qasm_program, m)?)?;
92 m.add_function(wrap_pyfunction!(compile_qasm_program_to_qir, m)?)?;
93 m.add_function(wrap_pyfunction!(compile_qasm_to_qsharp, m)?)?;
94 Ok(())
95}
96
97// This ordering must match the _native.pyi file.
98#[derive(Clone, Copy, Default, PartialEq)]
99#[pyclass(eq, eq_int, module = "qsharp._native")]
100#[allow(non_camel_case_types)]
101/// A Q# target profile.
102///
103/// A target profile describes the capabilities of the hardware or simulator
104/// which will be used to run the Q# program.
105pub(crate) enum TargetProfile {
106 /// Target supports the minimal set of capabilities required to run a quantum program.
107 ///
108 /// This option maps to the Base Profile as defined by the QIR specification.
109 #[default]
110 Base,
111 /// Target supports the Adaptive profile with the integer computation extension.
112 ///
113 /// This profile includes all of the required Adaptive Profile
114 /// capabilities, as well as the optional integer computation
115 /// extension defined by the QIR specification.
116 Adaptive_RI,
117 /// Target supports the Adaptive profile with integer & floating-point
118 /// computation extensions.
119 ///
120 /// This profile includes all required Adaptive Profile and `Adaptive_RI`
121 /// capabilities, as well as the optional floating-point computation
122 /// extension defined by the QIR specification.
123 Adaptive_RIF,
124 /// Target supports the full set of capabilities required to run any Q# program.
125 ///
126 /// This option maps to the Full Profile as defined by the QIR specification.
127 Unrestricted,
128}
129
130#[pymethods]
131impl TargetProfile {
132 #[new]
133 // We need to define `new` so that instances of `TargetProfile` can be created by Python
134 pub(crate) fn new() -> Self {
135 Self::default()
136 }
137
138 // called and the returned object is pickled as the contents for the instance
139 #[allow(clippy::trivially_copy_pass_by_ref)]
140 fn __getstate__(&self) -> PyResult<isize> {
141 Ok(self.__pyo3__int__())
142 }
143
144 // called with the unpickled state and the instance is updated in place
145 // This is what requires `new` to be implemented as we can't hydrate an
146 // unininitialized instance in Python.
147 fn __setstate__(&mut self, state: i32) -> PyResult<()> {
148 (*self) = match state {
149 0 => Self::Base,
150 1 => Self::Adaptive_RI,
151 2 => Self::Adaptive_RIF,
152 3 => Self::Unrestricted,
153 _ => return Err(PyValueError::new_err("invalid state")),
154 };
155 Ok(())
156 }
157
158 #[allow(clippy::trivially_copy_pass_by_ref)]
159 fn __str__(&self) -> String {
160 Into::<Profile>::into(*self).to_str().to_owned()
161 }
162
163 /// Creates a target profile from a string.
164 /// :param value: The string to parse.
165 /// :raises ValueError: If the string does not match any target profile.
166 #[classmethod]
167 #[allow(clippy::needless_pass_by_value)]
168 fn from_str(_cls: &Bound<'_, PyType>, key: String) -> pyo3::PyResult<Self> {
169 let profile = Profile::from_str(key.as_str())
170 .map_err(|()| PyValueError::new_err(format!("{key} is not a valid target profile")))?;
171 Ok(TargetProfile::from(profile))
172 }
173}
174
175impl From<Profile> for TargetProfile {
176 fn from(profile: Profile) -> Self {
177 match profile {
178 Profile::Base => TargetProfile::Base,
179 Profile::AdaptiveRI => TargetProfile::Adaptive_RI,
180 Profile::AdaptiveRIF => TargetProfile::Adaptive_RIF,
181 Profile::Unrestricted => TargetProfile::Unrestricted,
182 }
183 }
184}
185
186impl From<TargetProfile> for Profile {
187 fn from(profile: TargetProfile) -> Self {
188 match profile {
189 TargetProfile::Base => Profile::Base,
190 TargetProfile::Adaptive_RI => Profile::AdaptiveRI,
191 TargetProfile::Adaptive_RIF => Profile::AdaptiveRIF,
192 TargetProfile::Unrestricted => Profile::Unrestricted,
193 }
194 }
195}
196
197// This ordering must match the _native.pyi file.
198#[derive(Clone, Copy, Default, PartialEq)]
199#[pyclass(eq, eq_int, module = "qsharp._native")]
200#[allow(non_camel_case_types)]
201/// Represents the output semantics for OpenQASM 3 compilation.
202/// Each has implications on the output of the compilation
203/// and the semantic checks that are performed.
204pub(crate) enum OutputSemantics {
205 /// The output is in Qiskit format meaning that the output
206 /// is all of the classical registers, in reverse order
207 /// in which they were added to the circuit with each
208 /// bit within each register in reverse order.
209 #[default]
210 Qiskit,
211 /// [OpenQASM 3 has two output modes](https://openqasm.com/language/directives.html#input-output)
212 /// - If the programmer provides one or more `output` declarations, then
213 /// variables described as outputs will be returned as output.
214 /// The spec make no mention of endianness or order of the output.
215 /// - Otherwise, assume all of the declared variables are returned as output.
216 OpenQasm,
217 /// No output semantics are applied. The entry point returns `Unit`.
218 ResourceEstimation,
219}
220
221#[pymethods]
222impl OutputSemantics {
223 #[new]
224 // We need to define `new` so that instances of `TargetProfile` can be created by Python
225 pub(crate) fn new() -> Self {
226 Self::default()
227 }
228
229 // called and the returned object is pickled as the contents for the instance
230 #[allow(clippy::trivially_copy_pass_by_ref)]
231 fn __getstate__(&self) -> PyResult<isize> {
232 Ok(self.__pyo3__int__())
233 }
234
235 // called with the unpickled state and the instance is updated in place
236 // This is what requires `new` to be implemented as we can't hydrate an
237 // unininitialized instance in Python.
238 fn __setstate__(&mut self, state: i32) -> PyResult<()> {
239 (*self) = match state {
240 0 => Self::Qiskit,
241 1 => Self::OpenQasm,
242 2 => Self::ResourceEstimation,
243 _ => return Err(PyValueError::new_err("invalid state")),
244 };
245 Ok(())
246 }
247}
248
249impl From<OutputSemantics> for qsc::qasm::OutputSemantics {
250 fn from(output_semantics: OutputSemantics) -> Self {
251 match output_semantics {
252 OutputSemantics::Qiskit => qsc::qasm::OutputSemantics::Qiskit,
253 OutputSemantics::OpenQasm => qsc::qasm::OutputSemantics::OpenQasm,
254 OutputSemantics::ResourceEstimation => qsc::qasm::OutputSemantics::ResourceEstimation,
255 }
256 }
257}
258
259// This ordering must match the _native.pyi file.
260#[derive(Clone, Copy, Default, PartialEq)]
261#[pyclass(eq, eq_int, module = "qsharp._native")]
262#[allow(non_camel_case_types)]
263/// Represents the type of compilation output to create
264pub enum ProgramType {
265 /// Creates an operation in a namespace as if the program is a standalone
266 /// file. Inputs are lifted to the operation params. Output are lifted to
267 /// the operation return type. The operation is marked as `@EntryPoint`
268 /// as long as there are no input parameters.
269 #[default]
270 File,
271 /// Programs are compiled to a standalone function. Inputs are lifted to
272 /// the operation params. Output are lifted to the operation return type.
273 Operation,
274 /// Creates a list of statements from the program. This is useful for
275 /// interactive environments where the program is a list of statements
276 /// imported into the current scope.
277 /// This is also useful for testing individual statements compilation.
278 Fragments,
279}
280
281#[pymethods]
282impl ProgramType {
283 #[new]
284 // We need to define `new` so that instances of `TargetProfile` can be created by Python
285 pub(crate) fn new() -> Self {
286 Self::default()
287 }
288
289 // called and the returned object is pickled as the contents for the instance
290 #[allow(clippy::trivially_copy_pass_by_ref)]
291 fn __getstate__(&self) -> PyResult<isize> {
292 Ok(self.__pyo3__int__())
293 }
294
295 // called with the unpickled state and the instance is updated in place
296 // This is what requires `new` to be implemented as we can't hydrate an
297 // unininitialized instance in Python.
298 fn __setstate__(&mut self, state: i32) -> PyResult<()> {
299 (*self) = match state {
300 0 => Self::File,
301 1 => Self::Operation,
302 2 => Self::Fragments,
303 _ => return Err(PyValueError::new_err("invalid state")),
304 };
305 Ok(())
306 }
307}
308
309impl From<ProgramType> for qsc::qasm::ProgramType {
310 fn from(output_semantics: ProgramType) -> Self {
311 match output_semantics {
312 ProgramType::File => qsc::qasm::ProgramType::File,
313 ProgramType::Operation => qsc::qasm::ProgramType::Operation,
314 ProgramType::Fragments => qsc::qasm::ProgramType::Fragments,
315 }
316 }
317}
318
319#[allow(clippy::struct_field_names)]
320#[pyclass(unsendable)]
321pub(crate) struct Interpreter {
322 pub(crate) interpreter: interpret::Interpreter,
323 /// The Python function to call to create a new function wrapping a callable invocation.
324 pub(crate) make_callable: Option<PyObject>,
325}
326
327thread_local! { static PACKAGE_CACHE: Rc<RefCell<PackageCache>> = Rc::default(); }
328
329#[pymethods]
330/// A Q# interpreter.
331impl Interpreter {
332 #[allow(clippy::too_many_arguments)]
333 #[allow(clippy::needless_pass_by_value)]
334 #[pyo3(signature = (target_profile, language_features=None, project_root=None, read_file=None, list_directory=None, resolve_path=None, fetch_github=None, make_callable=None))]
335 #[new]
336 /// Initializes a new Q# interpreter.
337 pub(crate) fn new(
338 py: Python,
339 target_profile: TargetProfile,
340 language_features: Option<Vec<String>>,
341 project_root: Option<String>,
342 read_file: Option<PyObject>,
343 list_directory: Option<PyObject>,
344 resolve_path: Option<PyObject>,
345 fetch_github: Option<PyObject>,
346 make_callable: Option<PyObject>,
347 ) -> PyResult<Self> {
348 let target = Into::<Profile>::into(target_profile).into();
349
350 let language_features = LanguageFeatures::from_iter(language_features.unwrap_or_default());
351
352 let package_cache = PACKAGE_CACHE.with(Clone::clone);
353
354 let buildable_program = if let Some(project_root) = project_root {
355 if let (Some(read_file), Some(list_directory), Some(resolve_path), Some(fetch_github)) =
356 (read_file, list_directory, resolve_path, fetch_github)
357 {
358 let project =
359 file_system(py, read_file, list_directory, resolve_path, fetch_github)
360 .load_project(&PathBuf::from(project_root), Some(&package_cache))
361 .map_err(IntoPyErr::into_py_err)?;
362
363 if !project.errors.is_empty() {
364 return Err(project.errors.into_py_err());
365 }
366 let ProjectType::QSharp(package_graph_sources) = project.project_type else {
367 unreachable!("Project type should be Q#")
368 };
369 BuildableProgram::new(target, package_graph_sources)
370 } else {
371 panic!("file system hooks should have been passed in with a manifest descriptor")
372 }
373 } else {
374 let graph = PackageGraphSources::with_no_dependencies(
375 Vec::default(),
376 LanguageFeatures::from_iter(language_features),
377 None,
378 );
379 BuildableProgram::new(target, graph)
380 };
381
382 match interpret::Interpreter::new(
383 SourceMap::new(buildable_program.user_code.sources, None),
384 PackageType::Lib,
385 target,
386 buildable_program.user_code.language_features,
387 buildable_program.store,
388 &buildable_program.user_code_dependencies,
389 ) {
390 Ok(interpreter) => {
391 if let Some(make_callable) = &make_callable {
392 // Add any global callables from the user source as Python functions to the environment.
393 let exported_items = interpreter.user_globals();
394 for (namespace, name, val) in exported_items {
395 create_py_callable(py, make_callable, &namespace, &name, val)?;
396 }
397 }
398 Ok(Self {
399 interpreter,
400 make_callable,
401 })
402 }
403 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
404 }
405 }
406
407 /// Interprets Q# source code.
408 ///
409 /// :param input: The Q# source code to interpret.
410 /// :param output_fn: A callback function that will be called with each output.
411 ///
412 /// :returns value: The value returned by the last statement in the input.
413 ///
414 /// :raises QSharpError: If there is an error interpreting the input.
415 #[pyo3(signature=(input, callback=None))]
416 fn interpret(
417 &mut self,
418 py: Python,
419 input: &str,
420 callback: Option<PyObject>,
421 ) -> PyResult<PyObject> {
422 let mut receiver = OptionalCallbackReceiver { callback, py };
423 match self.interpreter.eval_fragments(&mut receiver, input) {
424 Ok(value) => {
425 if let Some(make_callable) = &self.make_callable {
426 // Get any global callables from the evaluated input and add them to the environment. This will grab
427 // every callable that was defined in the input and by previous calls that added to the open package.
428 // This is safe because either the callable will be replaced with itself or a new callable with the
429 // same name will shadow the previous one, which is the expected behavior.
430 let new_items = self.interpreter.source_globals();
431 for (namespace, name, val) in new_items {
432 create_py_callable(py, make_callable, &namespace, &name, val)?;
433 }
434 }
435 Ok(ValueWrapper(value).into_pyobject(py)?.unbind())
436 }
437 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
438 }
439 }
440
441 /// Imports OpenQASM source code into the active Q# interpreter.
442 ///
443 /// Args:
444 /// source (str): An OpenQASM program or fragment.
445 /// output_fn: The function to handle the output of the execution.
446 /// read_file: A callable that reads a file and returns its content and path.
447 /// list_directory: A callable that lists the contents of a directory.
448 /// resolve_path: A callable that resolves a file path given a base path and a relative path.
449 /// fetch_github: A callable that fetches a file from GitHub.
450 /// **kwargs: Additional keyword arguments to pass to the execution.
451 /// - name (str): The name of the program. This is used as the entry point for the program.
452 /// - search_path (Optional[str]): The optional search path for resolving file references.
453 /// - output_semantics (OutputSemantics, optional): The output semantics for the compilation.
454 /// - program_type (ProgramType, optional): The type of program compilation to perform.
455 ///
456 /// Returns:
457 /// value: The value returned by the last statement in the source code.
458 ///
459 /// Raises:
460 /// QasmError: If there is an error generating, parsing, or analyzing the OpenQASM source.
461 /// QSharpError: If there is an error compiling the program.
462 /// QSharpError: If there is an error evaluating the source code.
463 #[pyo3(signature=(input, output_fn, read_file, list_directory, resolve_path, fetch_github, **kwargs))]
464 #[allow(clippy::needless_pass_by_value)]
465 #[allow(clippy::too_many_arguments)]
466 fn import_qasm(
467 &mut self,
468 py: Python,
469 input: &str,
470 output_fn: Option<PyObject>,
471 read_file: Option<PyObject>,
472 list_directory: Option<PyObject>,
473 resolve_path: Option<PyObject>,
474 fetch_github: Option<PyObject>,
475 kwargs: Option<Bound<'_, PyDict>>,
476 ) -> PyResult<PyObject> {
477 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
478
479 let operation_name = get_operation_name(&kwargs)?;
480 let search_path = get_search_path(&kwargs)?;
481 let program_ty = get_program_type(&kwargs, || ProgramType::Operation)?;
482 let output_semantics = get_output_semantics(&kwargs, || OutputSemantics::OpenQasm)?;
483
484 let fs =
485 create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
486 let file_path = PathBuf::from_str(&search_path)
487 .expect("from_str is infallible")
488 .join("program.qasm");
489 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(input)));
490 let ProjectType::OpenQASM(sources) = project.project_type else {
491 return Err(QasmError::new_err(
492 "Expected OpenQASM project, but got a different type".to_string(),
493 ));
494 };
495
496 let config = CompilerConfig::new(
497 QubitSemantics::Qiskit,
498 output_semantics.into(),
499 program_ty.into(),
500 Some(operation_name.into()),
501 None,
502 );
503 let res = qsc::qasm::semantic::parse_sources(&sources);
504 let unit = compile_to_qsharp_ast_with_config(res, config);
505 let (sources, errors, package, _) = unit.into_tuple();
506
507 if !errors.is_empty() {
508 let errors = errors
509 .iter()
510 .map(|e| {
511 use qsc::compile::ErrorKind;
512 use qsc::interpret::Error;
513 let error = e.error().clone();
514 let kind = ErrorKind::OpenQasm(error);
515 let v = WithSource::from_map(&sources, kind);
516 Error::Compile(v)
517 })
518 .collect();
519 return Err(QSharpError::new_err(format_errors(errors)));
520 }
521 let mut receiver = OptionalCallbackReceiver {
522 callback: output_fn,
523 py,
524 };
525
526 match self
527 .interpreter
528 .eval_ast_fragments(&mut receiver, input, package)
529 {
530 Ok(value) => {
531 if let Some(make_callable) = &self.make_callable {
532 // Get any global callables from the evaluated input and add them to the environment. This will grab
533 // every callable that was defined in the input and by previous calls that added to the open package.
534 // This is safe because either the callable will be replaced with itself or a new callable with the
535 // same name will shadow the previous one, which is the expected behavior.
536 let new_items = self.interpreter.source_globals();
537 for (namespace, name, val) in new_items {
538 create_py_callable(py, make_callable, &namespace, &name, val)?;
539 }
540 }
541 Ok(ValueWrapper(value).into_pyobject(py)?.unbind())
542 }
543 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
544 }
545 }
546
547 /// Sets the quantum seed for the interpreter.
548 #[pyo3(signature=(seed=None))]
549 fn set_quantum_seed(&mut self, seed: Option<u64>) {
550 self.interpreter.set_quantum_seed(seed);
551 }
552
553 /// Sets the classical seed for the interpreter.
554 #[pyo3(signature=(seed=None))]
555 fn set_classical_seed(&mut self, seed: Option<u64>) {
556 self.interpreter.set_classical_seed(seed);
557 }
558
559 /// Dumps the quantum state of the interpreter.
560 /// Returns a tuple of (amplitudes, num_qubits), where amplitudes is a dictionary from integer indices to
561 /// pairs of real and imaginary amplitudes.
562 fn dump_machine(&mut self) -> StateDumpData {
563 let (state, qubit_count) = self.interpreter.get_quantum_state();
564 StateDumpData(DisplayableState(state, qubit_count))
565 }
566
567 /// Dumps the current circuit state of the interpreter.
568 ///
569 /// This circuit will contain the gates that have been applied
570 /// in the simulator up to the current point.
571 fn dump_circuit(&mut self, py: Python) -> PyResult<PyObject> {
572 Circuit(self.interpreter.get_circuit()).into_py_any(py)
573 }
574
575 #[pyo3(signature=(entry_expr=None, callback=None, noise=None, callable=None, args=None))]
576 fn run(
577 &mut self,
578 py: Python,
579 entry_expr: Option<&str>,
580 callback: Option<PyObject>,
581 noise: Option<(f64, f64, f64)>,
582 callable: Option<GlobalCallable>,
583 args: Option<PyObject>,
584 ) -> PyResult<PyObject> {
585 let mut receiver = OptionalCallbackReceiver { callback, py };
586
587 let noise = match noise {
588 None => None,
589 Some((px, py, pz)) => match PauliNoise::from_probabilities(px, py, pz) {
590 Ok(noise_struct) => Some(noise_struct),
591 Err(error_message) => return Err(PyException::new_err(error_message)),
592 },
593 };
594
595 let result = match callable {
596 Some(callable) => {
597 let (input_ty, output_ty) = self
598 .interpreter
599 .global_tys(&callable.0)
600 .ok_or(QSharpError::new_err("callable not found"))?;
601 let args = args_to_values(py, args, &input_ty, &output_ty)?;
602 self.interpreter
603 .invoke_with_noise(&mut receiver, callable.0, args, noise)
604 }
605 _ => self.interpreter.run(&mut receiver, entry_expr, noise),
606 };
607
608 match result {
609 Ok(value) => Ok(ValueWrapper(value).into_pyobject(py)?.unbind()),
610 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
611 }
612 }
613
614 #[pyo3(signature=(callable, args=None, callback=None))]
615 fn invoke(
616 &mut self,
617 py: Python,
618 callable: GlobalCallable,
619 args: Option<PyObject>,
620 callback: Option<PyObject>,
621 ) -> PyResult<PyObject> {
622 let mut receiver = OptionalCallbackReceiver { callback, py };
623 let (input_ty, output_ty) = self
624 .interpreter
625 .global_tys(&callable.0)
626 .ok_or(QSharpError::new_err("callable not found"))?;
627
628 let args = args_to_values(py, args, &input_ty, &output_ty)?;
629
630 match self.interpreter.invoke(&mut receiver, callable.0, args) {
631 Ok(value) => Ok(ValueWrapper(value).into_pyobject(py)?.unbind()),
632 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
633 }
634 }
635
636 #[pyo3(signature=(entry_expr=None, callable=None, args=None))]
637 fn qir(
638 &mut self,
639 py: Python,
640 entry_expr: Option<&str>,
641 callable: Option<GlobalCallable>,
642 args: Option<PyObject>,
643 ) -> PyResult<String> {
644 if let Some(entry_expr) = entry_expr {
645 match self.interpreter.qirgen(entry_expr) {
646 Ok(qir) => Ok(qir),
647 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
648 }
649 } else {
650 let callable = callable.ok_or_else(|| {
651 QSharpError::new_err("either entry_expr or callable must be specified")
652 })?;
653 let (input_ty, output_ty) = self
654 .interpreter
655 .global_tys(&callable.0)
656 .ok_or(QSharpError::new_err("callable not found"))?;
657
658 let args = args_to_values(py, args, &input_ty, &output_ty)?;
659 match self.interpreter.qirgen_from_callable(&callable.0, args) {
660 Ok(qir) => Ok(qir),
661 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
662 }
663 }
664 }
665
666 /// Synthesizes a circuit for a Q# program. Either an entry
667 /// expression or an operation must be provided.
668 ///
669 /// :param entry_expr: An entry expression.
670 ///
671 /// :param operation: The operation to synthesize. This can be a name of
672 /// an operation of a lambda expression. The operation must take only
673 /// qubits or arrays of qubits as parameters.
674 ///
675 /// :param callable: A callable to synthesize.
676 ///
677 /// :param args: The arguments to pass to the callable.
678 ///
679 /// :raises QSharpError: If there is an error synthesizing the circuit.
680 #[pyo3(signature=(entry_expr=None, operation=None, callable=None, args=None))]
681 fn circuit(
682 &mut self,
683 py: Python,
684 entry_expr: Option<String>,
685 operation: Option<String>,
686 callable: Option<GlobalCallable>,
687 args: Option<PyObject>,
688 ) -> PyResult<PyObject> {
689 let entrypoint = match (entry_expr, operation, callable) {
690 (Some(entry_expr), None, None) => CircuitEntryPoint::EntryExpr(entry_expr),
691 (None, Some(operation), None) => CircuitEntryPoint::Operation(operation),
692 (None, None, Some(callable)) => {
693 let (input_ty, output_ty) = self
694 .interpreter
695 .global_tys(&callable.0)
696 .ok_or(QSharpError::new_err("callable not found"))?;
697 let args = args_to_values(py, args, &input_ty, &output_ty)?;
698 CircuitEntryPoint::Callable(callable.0, args)
699 }
700 _ => {
701 return Err(PyException::new_err(
702 "either entry_expr or operation must be specified",
703 ))
704 }
705 };
706
707 match self.interpreter.circuit(entrypoint, false) {
708 Ok(circuit) => Circuit(circuit).into_py_any(py),
709 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
710 }
711 }
712
713 #[pyo3(signature=(job_params, entry_expr=None, callable=None, args=None))]
714 fn estimate(
715 &mut self,
716 py: Python,
717 job_params: &str,
718 entry_expr: Option<&str>,
719 callable: Option<GlobalCallable>,
720 args: Option<PyObject>,
721 ) -> PyResult<String> {
722 let results = if let Some(entry_expr) = entry_expr {
723 estimate_expr(&mut self.interpreter, entry_expr, job_params)
724 } else {
725 let callable = callable.ok_or_else(|| {
726 QSharpError::new_err("either entry_expr or callable must be specified")
727 })?;
728 let (input_ty, output_ty) = self
729 .interpreter
730 .global_tys(&callable.0)
731 .ok_or(QSharpError::new_err("callable not found"))?;
732 let args = args_to_values(py, args, &input_ty, &output_ty)?;
733 estimate_call(&mut self.interpreter, callable.0, args, job_params)
734 };
735 match results {
736 Ok(estimate) => Ok(estimate),
737 Err(errors) if matches!(errors[0], re::Error::Interpreter(_)) => {
738 Err(QSharpError::new_err(format_errors(
739 errors
740 .into_iter()
741 .map(|e| match e {
742 re::Error::Interpreter(e) => e,
743 re::Error::Estimation(_) => unreachable!(),
744 })
745 .collect::<Vec<_>>(),
746 )))
747 }
748 Err(errors) => Err(QSharpError::new_err(
749 errors
750 .into_iter()
751 .map(|e| match e {
752 re::Error::Estimation(e) => e.to_string(),
753 re::Error::Interpreter(_) => unreachable!(),
754 })
755 .collect::<Vec<_>>()
756 .join("\n"),
757 )),
758 }
759 }
760}
761
762fn args_to_values(
763 py: Python,
764 args: Option<PyObject>,
765 input_ty: &Ty,
766 output_ty: &Ty,
767) -> PyResult<Value> {
768 // If the types are not supported, we can't convert the arguments or return value.
769 // Check this before trying to convert the arguments, and return an error if the types are not supported.
770 if let Some(ty) = first_unsupported_interop_ty(input_ty) {
771 return Err(QSharpError::new_err(format!(
772 "unsupported input type: `{ty}`"
773 )));
774 }
775 if let Some(ty) = first_unsupported_interop_ty(output_ty) {
776 return Err(QSharpError::new_err(format!(
777 "unsupported output type: `{ty}`"
778 )));
779 }
780
781 // Conver the Python arguments to Q# values, treating None as an empty tuple aka `Unit`.
782 if matches!(&input_ty, Ty::Tuple(tup) if tup.is_empty()) {
783 // Special case for unit, where args should be None
784 if args.is_some() {
785 return Err(QSharpError::new_err("expected no arguments"));
786 }
787 Ok(Value::unit())
788 } else {
789 let Some(args) = args else {
790 return Err(QSharpError::new_err(format!(
791 "expected arguments of type `{input_ty}`"
792 )));
793 };
794 // This conversion will produce errors if the types don't match or can't be converted.
795 Ok(convert_obj_with_ty(py, &args, input_ty)?)
796 }
797}
798
799/// Finds any Q# type recursively that does not support interop with Python, meaning our code cannot convert it back and forth
800/// across the interop boundary.
801fn first_unsupported_interop_ty(ty: &Ty) -> Option<&Ty> {
802 match ty {
803 Ty::Prim(prim_ty) => match prim_ty {
804 Prim::Pauli
805 | Prim::BigInt
806 | Prim::Bool
807 | Prim::Double
808 | Prim::Int
809 | Prim::String
810 | Prim::Result => None,
811 Prim::Qubit | Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => {
812 Some(ty)
813 }
814 },
815 Ty::Tuple(tup) => tup
816 .iter()
817 .find(|t| first_unsupported_interop_ty(t).is_some()),
818 Ty::Array(ty) => first_unsupported_interop_ty(ty),
819 _ => Some(ty),
820 }
821}
822
823/// Given a type, convert a Python object into a Q# value of that type. This will recur through tuples and arrays,
824/// and will return an error if the type is not supported or the object cannot be converted.
825fn convert_obj_with_ty(py: Python, obj: &PyObject, ty: &Ty) -> PyResult<Value> {
826 match ty {
827 Ty::Prim(prim_ty) => match prim_ty {
828 Prim::BigInt => Ok(Value::BigInt(obj.extract::<BigInt>(py)?)),
829 Prim::Bool => Ok(Value::Bool(obj.extract::<bool>(py)?)),
830 Prim::Double => Ok(Value::Double(obj.extract::<f64>(py)?)),
831 Prim::Int => Ok(Value::Int(obj.extract::<i64>(py)?)),
832 Prim::String => Ok(Value::String(obj.extract::<String>(py)?.into())),
833 Prim::Result => Ok(Value::Result(qsc::interpret::Result::Val(
834 obj.extract::<Result>(py)? == Result::One,
835 ))),
836 Prim::Pauli => Ok(Value::Pauli(match obj.extract::<Pauli>(py)? {
837 Pauli::I => fir::Pauli::I,
838 Pauli::X => fir::Pauli::X,
839 Pauli::Y => fir::Pauli::Y,
840 Pauli::Z => fir::Pauli::Z,
841 })),
842 Prim::Qubit | Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => {
843 unimplemented!("primitive input type: {prim_ty:?}")
844 }
845 },
846 Ty::Tuple(tup) => {
847 if tup.len() == 1 {
848 let value = convert_obj_with_ty(py, obj, &tup[0]);
849 Ok(Value::Tuple(vec![value?].into()))
850 } else {
851 let obj = obj.extract::<Vec<PyObject>>(py)?;
852 if obj.len() != tup.len() {
853 return Err(QSharpError::new_err(format!(
854 "mismatched tuple arity: expected {}, got {}",
855 tup.len(),
856 obj.len()
857 )));
858 }
859 let mut values = Vec::with_capacity(obj.len());
860 for (i, ty) in tup.iter().enumerate() {
861 values.push(convert_obj_with_ty(py, &obj[i], ty)?);
862 }
863 Ok(Value::Tuple(values.into()))
864 }
865 }
866 Ty::Array(ty) => {
867 let obj = obj.extract::<Vec<PyObject>>(py)?;
868 let mut values = Vec::with_capacity(obj.len());
869 for item in &obj {
870 values.push(convert_obj_with_ty(py, item, ty)?);
871 }
872 Ok(Value::Array(values.into()))
873 }
874 _ => unimplemented!("input type: {ty}"),
875 }
876}
877
878#[pyfunction]
879pub fn physical_estimates(logical_resources: &str, job_params: &str) -> PyResult<String> {
880 match re::estimate_physical_resources_from_json(logical_resources, job_params) {
881 Ok(estimates) => Ok(estimates),
882 Err(error) => Err(QSharpError::new_err(error.to_string())),
883 }
884}
885
886create_exception!(
887 module,
888 QSharpError,
889 pyo3::exceptions::PyException,
890 "An error returned from the Q# interpreter."
891);
892
893create_exception!(
894 module,
895 QasmError,
896 pyo3::exceptions::PyException,
897 "An error returned from the OpenQASM parser."
898);
899
900pub(crate) fn format_errors(errors: Vec<interpret::Error>) -> String {
901 errors
902 .into_iter()
903 .map(|e| format_error(&e))
904 .collect::<Vec<_>>()
905 .join("\n")
906}
907
908pub(crate) fn format_error(e: &interpret::Error) -> String {
909 let mut message = String::new();
910 if let Some(stack_trace) = e.stack_trace() {
911 write!(message, "{stack_trace}").unwrap();
912 }
913 let additional_help = python_help(e);
914 let report = Report::new(e.clone());
915 write!(message, "{report:?}")
916 .unwrap_or_else(|err| panic!("writing error failed: {err} error was: {e:?}"));
917 if let Some(additional_help) = additional_help {
918 writeln!(message, "{additional_help}").unwrap();
919 }
920 message
921}
922
923/// Additional help text for an error specific to the Python module
924fn python_help(error: &interpret::Error) -> Option<String> {
925 if matches!(error, interpret::Error::UnsupportedRuntimeCapabilities) {
926 Some("Unsupported target profile. Initialize Q# by running `qsharp.init(target_profile=qsharp.TargetProfile.Base)` before performing code generation.".into())
927 } else {
928 None
929 }
930}
931
932#[pyclass]
933pub(crate) struct Output(DisplayableOutput);
934
935#[pymethods]
936/// An output returned from the Q# interpreter.
937/// Outputs can be a state dumps or messages. These are normally printed to the console.
938impl Output {
939 fn __repr__(&self) -> String {
940 match &self.0 {
941 DisplayableOutput::State(state) => state.to_plain(),
942 DisplayableOutput::Matrix(matrix) => matrix.to_plain(),
943 DisplayableOutput::Message(msg) => msg.clone(),
944 }
945 }
946
947 fn __str__(&self) -> String {
948 self.__repr__()
949 }
950
951 fn _repr_markdown_(&self) -> Option<String> {
952 match &self.0 {
953 DisplayableOutput::State(state) => {
954 let latex = if let Some(latex) = state.to_latex() {
955 format!("\n\n{latex}")
956 } else {
957 String::default()
958 };
959 Some(format!("{}{latex}", state.to_html()))
960 }
961 DisplayableOutput::Message(_) => None,
962 DisplayableOutput::Matrix(matrix) => Some(matrix.to_latex()),
963 }
964 }
965
966 fn state_dump(&self) -> Option<StateDumpData> {
967 match &self.0 {
968 DisplayableOutput::State(state) => Some(StateDumpData(state.clone())),
969 DisplayableOutput::Matrix(_) | DisplayableOutput::Message(_) => None,
970 }
971 }
972
973 fn is_state_dump(&self) -> bool {
974 matches!(&self.0, DisplayableOutput::State(_))
975 }
976
977 fn is_matrix(&self) -> bool {
978 matches!(&self.0, DisplayableOutput::Matrix(_))
979 }
980
981 fn is_message(&self) -> bool {
982 matches!(&self.0, DisplayableOutput::Message(_))
983 }
984}
985
986#[pyclass]
987/// Captured simlation state dump.
988pub(crate) struct StateDumpData(pub(crate) DisplayableState);
989
990#[pymethods]
991impl StateDumpData {
992 fn get_dict<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyDict>> {
993 let dict = rustc_hash::FxHashMap::from_iter(self.0 .0.clone());
994 dict.into_pyobject(py)
995 }
996
997 #[getter]
998 fn get_qubit_count(&self) -> usize {
999 self.0 .1
1000 }
1001
1002 fn __len__(&self) -> usize {
1003 self.0 .0.len()
1004 }
1005
1006 fn __repr__(&self) -> String {
1007 self.0.to_plain()
1008 }
1009
1010 fn __str__(&self) -> String {
1011 self.__repr__()
1012 }
1013
1014 fn _repr_markdown_(&self) -> String {
1015 let latex = if let Some(latex) = self.0.to_latex() {
1016 format!("\n\n{latex}")
1017 } else {
1018 String::default()
1019 };
1020 format!("{}{latex}", self.0.to_html())
1021 }
1022
1023 fn _repr_latex_(&self) -> Option<String> {
1024 self.0.to_latex()
1025 }
1026}
1027
1028#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1029#[pyclass(eq, eq_int, ord)]
1030/// A Q# measurement result.
1031pub(crate) enum Result {
1032 Zero,
1033 One,
1034}
1035
1036#[pymethods]
1037impl Result {
1038 #[allow(clippy::trivially_copy_pass_by_ref)]
1039 fn __repr__(&self) -> String {
1040 match self {
1041 Result::Zero => "Zero".to_owned(),
1042 Result::One => "One".to_owned(),
1043 }
1044 }
1045
1046 #[allow(clippy::trivially_copy_pass_by_ref)]
1047 fn __str__(&self) -> String {
1048 self.__repr__()
1049 }
1050
1051 #[allow(clippy::trivially_copy_pass_by_ref)]
1052 fn __hash__(&self) -> u32 {
1053 match self {
1054 Result::Zero => 0,
1055 Result::One => 1,
1056 }
1057 }
1058}
1059
1060#[derive(Clone, Copy, PartialEq)]
1061#[pyclass(eq, eq_int)]
1062/// A Q# Pauli operator.
1063pub(crate) enum Pauli {
1064 I,
1065 X,
1066 Y,
1067 Z,
1068}
1069
1070// Mapping of Q# value types to Python value types.
1071pub(crate) struct ValueWrapper(pub(crate) Value);
1072
1073impl<'py> IntoPyObject<'py> for ValueWrapper {
1074 type Target = PyAny;
1075
1076 type Output = Bound<'py, Self::Target>;
1077
1078 type Error = pyo3::PyErr;
1079
1080 fn into_pyobject(self, py: Python<'py>) -> std::result::Result<Self::Output, Self::Error> {
1081 match self.0 {
1082 Value::Int(val) => val.into_bound_py_any(py),
1083 Value::BigInt(val) => val.into_bound_py_any(py),
1084 Value::Double(val) => val.into_bound_py_any(py),
1085 Value::Bool(val) => val.into_bound_py_any(py),
1086 Value::String(val) => val.into_bound_py_any(py),
1087 Value::Result(val) => if val.unwrap_bool() {
1088 Result::One
1089 } else {
1090 Result::Zero
1091 }
1092 .into_bound_py_any(py),
1093 Value::Pauli(val) => match val {
1094 fir::Pauli::I => Pauli::I.into_bound_py_any(py),
1095 fir::Pauli::X => Pauli::X.into_bound_py_any(py),
1096 fir::Pauli::Y => Pauli::Y.into_bound_py_any(py),
1097 fir::Pauli::Z => Pauli::Z.into_bound_py_any(py),
1098 },
1099 Value::Tuple(val) => {
1100 if val.is_empty() {
1101 // Special case Value::unit as None
1102 Ok(py.None().into_bound(py))
1103 } else {
1104 PyTuple::new(py, val.iter().map(|v| ValueWrapper(v.clone())))?
1105 .into_bound_py_any(py)
1106 }
1107 }
1108 Value::Array(val) => {
1109 PyList::new(py, val.iter().map(|v| ValueWrapper(v.clone())))?.into_bound_py_any(py)
1110 }
1111 _ => format!("<{}> {}", Value::type_name(&self.0), &self.0).into_bound_py_any(py),
1112 }
1113 }
1114}
1115
1116pub(crate) struct OptionalCallbackReceiver<'a> {
1117 pub(crate) callback: Option<PyObject>,
1118 pub(crate) py: Python<'a>,
1119}
1120
1121impl Receiver for OptionalCallbackReceiver<'_> {
1122 fn state(
1123 &mut self,
1124 state: Vec<(BigUint, Complex64)>,
1125 qubit_count: usize,
1126 ) -> core::result::Result<(), Error> {
1127 if let Some(callback) = &self.callback {
1128 let out = DisplayableOutput::State(DisplayableState(state, qubit_count));
1129 callback
1130 .call1(
1131 self.py,
1132 PyTuple::new(
1133 self.py,
1134 &[Py::new(self.py, Output(out)).expect("should be able to create output")],
1135 )
1136 .map_err(|_| Error)?,
1137 )
1138 .map_err(|_| Error)?;
1139 }
1140 Ok(())
1141 }
1142
1143 fn matrix(&mut self, matrix: Vec<Vec<Complex64>>) -> std::result::Result<(), Error> {
1144 if let Some(callback) = &self.callback {
1145 let out = DisplayableOutput::Matrix(DisplayableMatrix(matrix));
1146 callback
1147 .call1(
1148 self.py,
1149 PyTuple::new(
1150 self.py,
1151 &[Py::new(self.py, Output(out)).expect("should be able to create output")],
1152 )
1153 .map_err(|_| Error)?,
1154 )
1155 .map_err(|_| Error)?;
1156 }
1157 Ok(())
1158 }
1159
1160 fn message(&mut self, msg: &str) -> core::result::Result<(), Error> {
1161 if let Some(callback) = &self.callback {
1162 let out = DisplayableOutput::Message(msg.to_owned());
1163 callback
1164 .call1(
1165 self.py,
1166 PyTuple::new(
1167 self.py,
1168 &[Py::new(self.py, Output(out)).expect("should be able to create output")],
1169 )
1170 .map_err(|_| Error)?,
1171 )
1172 .map_err(|_| Error)?;
1173 }
1174 Ok(())
1175 }
1176}
1177
1178#[pyclass]
1179pub(crate) struct Circuit(pub qsc::circuit::Circuit);
1180
1181#[pymethods]
1182impl Circuit {
1183 fn __repr__(&self) -> String {
1184 self.0.to_string()
1185 }
1186
1187 fn __str__(&self) -> String {
1188 self.__repr__()
1189 }
1190
1191 fn json(&self, _py: Python) -> PyResult<String> {
1192 serde_json::to_string(&self.0).map_err(|e| PyException::new_err(e.to_string()))
1193 }
1194}
1195
1196trait IntoPyErr {
1197 fn into_py_err(self) -> PyErr;
1198}
1199
1200impl IntoPyErr for Report {
1201 fn into_py_err(self) -> PyErr {
1202 PyException::new_err(format!("{self:?}"))
1203 }
1204}
1205
1206impl<E> IntoPyErr for Vec<E>
1207where
1208 E: Diagnostic + Send + Sync + 'static,
1209{
1210 fn into_py_err(self) -> PyErr {
1211 let mut message = String::new();
1212 for diag in self {
1213 let report = Report::new(diag);
1214 writeln!(message, "{report:?}").expect("string should be writable");
1215 }
1216 PyException::new_err(message)
1217 }
1218}
1219
1220#[pyclass(unsendable)]
1221#[derive(Clone)]
1222struct GlobalCallable(Value);
1223
1224impl From<Value> for GlobalCallable {
1225 fn from(val: Value) -> Self {
1226 match val {
1227 val @ Value::Global(..) => GlobalCallable(val),
1228 _ => panic!("expected global callable"),
1229 }
1230 }
1231}
1232
1233impl From<GlobalCallable> for Value {
1234 fn from(val: GlobalCallable) -> Self {
1235 val.0
1236 }
1237}
1238
1239/// Create a Python callable from a Q# callable and adds it to the given environment.
1240fn create_py_callable(
1241 py: Python,
1242 make_callable: &PyObject,
1243 namespace: &[Rc<str>],
1244 name: &str,
1245 val: Value,
1246) -> PyResult<()> {
1247 if namespace.is_empty() && name == "<lambda>" {
1248 // We don't want to bind auto-generated lambda callables.
1249 return Ok(());
1250 }
1251
1252 let args = (
1253 Py::new(py, GlobalCallable::from(val)).expect("should be able to create callable"), // callable id
1254 PyList::new(py, namespace.iter().map(ToString::to_string))?, // namespace as string array
1255 PyString::new(py, name), // name of callable
1256 );
1257
1258 // Call into the Python layer to create the function wrapping the callable invocation.
1259 make_callable.call1(py, args)?;
1260
1261 Ok(())
1262}
1263