microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
iadavis/pipeline-issue-debugging

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/interpreter.rs

1500lines · modecode

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