microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.19.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/interpreter.rs

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