microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
4fa10c30a716d7fc2f67a0a385f3da565daa1237

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/interpreter.rs

1548lines · modecode

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