microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
joaoboechat/remove-web-worker-dependency

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/interpreter.rs

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