microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
billti/qdk_package

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/interpreter.rs

1294lines · modecode

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