microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
brlackey/cultivation-models

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/interpreter.rs

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