microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fedimser/is-re

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/interpreter.rs

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