microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/replace-qsharp-with-qdk-python-tests

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/interpreter.rs

1585lines · 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, 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))]
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 ) -> PyResult<Py<PyAny>> {
727 let mut receiver = OptionalCallbackReceiver { callback, py };
728
729 let callable_val = if let Some(callable) = callable {
730 Some(extract_callable_value(py, &callable)?)
731 } else {
732 None
733 };
734
735 let noise = match noise {
736 None => None,
737 Some((px, py, pz)) => match PauliNoise::from_probabilities(px, py, pz) {
738 Ok(noise_struct) => Some(noise_struct),
739 Err(error_message) => return Err(PyException::new_err(error_message)),
740 },
741 };
742
743 // Convert NoiseConfig to a rust NoiseConfig.
744 let noise_config: Option<qdk_simulators::noise_config::NoiseConfig<f64, f64>> =
745 noise_config.map(|noise_config| unbind_noise_config(py, noise_config));
746
747 let result = match callable_val {
748 Some(callable) => {
749 let (input_ty, output_ty) = self
750 .interpreter
751 .global_callable_ty(&callable)
752 .ok_or(QSharpError::new_err("callable not found"))?;
753 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
754
755 self.interpreter.invoke_with_noise(
756 &mut receiver,
757 callable,
758 args,
759 noise,
760 qubit_loss,
761 noise_config,
762 seed,
763 )
764 }
765 _ => self.interpreter.run(
766 &mut receiver,
767 entry_expr,
768 noise,
769 qubit_loss,
770 noise_config,
771 seed,
772 ),
773 };
774
775 match result {
776 Ok(value) => value_to_pyobj(&self.interpreter, py, &value),
777 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
778 }
779 }
780
781 #[pyo3(signature=(callable, args=None, callback=None))]
782 #[allow(clippy::needless_pass_by_value)]
783 fn invoke(
784 &mut self,
785 py: Python,
786 callable: Py<PyAny>,
787 args: Option<Py<PyAny>>,
788 callback: Option<Py<PyAny>>,
789 ) -> PyResult<Py<PyAny>> {
790 let callable = extract_callable_value(py, &callable)?;
791 let mut receiver = OptionalCallbackReceiver { callback, py };
792 let (input_ty, output_ty) = self
793 .interpreter
794 .global_callable_ty(&callable)
795 .ok_or(QSharpError::new_err("callable not found"))?;
796
797 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
798
799 match self.interpreter.invoke(&mut receiver, callable, args) {
800 Ok(value) => value_to_pyobj(&self.interpreter, py, &value),
801 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
802 }
803 }
804
805 #[pyo3(signature=(entry_expr=None, callable=None, args=None))]
806 fn qir(
807 &mut self,
808 py: Python,
809 entry_expr: Option<&str>,
810 callable: Option<Py<PyAny>>,
811 args: Option<Py<PyAny>>,
812 ) -> PyResult<String> {
813 if let Some(entry_expr) = entry_expr {
814 match self.interpreter.qirgen(entry_expr) {
815 Ok(qir) => Ok(qir),
816 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
817 }
818 } else {
819 let callable = callable.ok_or_else(|| {
820 QSharpError::new_err("either entry_expr or callable must be specified")
821 })?;
822 let callable = extract_callable_value(py, &callable)?;
823 let (input_ty, output_ty) = self
824 .interpreter
825 .global_callable_ty(&callable)
826 .ok_or(QSharpError::new_err("callable not found"))?;
827
828 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
829 match self.interpreter.qirgen_from_callable(&callable, args) {
830 Ok(qir) => Ok(qir),
831 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
832 }
833 }
834 }
835
836 /// Synthesizes a circuit for a Q# program. Either an entry
837 /// expression or an operation must be provided.
838 ///
839 /// :param config: Circuit generation options.
840 ///
841 /// :param entry_expr: An entry expression.
842 ///
843 /// :param operation: The operation to synthesize. This can be a name of
844 /// an operation of a lambda expression. The operation must take only
845 /// qubits or arrays of qubits as parameters.
846 ///
847 /// :param callable: A callable to synthesize.
848 ///
849 /// :param args: The arguments to pass to the callable.
850 ///
851 /// :raises QSharpError: If there is an error synthesizing the circuit.
852 #[pyo3(signature=(config, entry_expr=None,*, operation=None, callable=None, args=None))]
853 fn circuit(
854 &mut self,
855 py: Python,
856 config: &CircuitConfig,
857 entry_expr: Option<String>,
858 operation: Option<String>,
859 callable: Option<Py<PyAny>>,
860 args: Option<Py<PyAny>>,
861 ) -> PyResult<Py<PyAny>> {
862 let entrypoint = match (entry_expr, operation, callable) {
863 (Some(entry_expr), None, None) => CircuitEntryPoint::EntryExpr(entry_expr),
864 (None, Some(operation), None) => CircuitEntryPoint::Operation(operation),
865 (None, None, Some(callable)) => {
866 let callable = extract_callable_value(py, &callable)?;
867 let (input_ty, output_ty) = self
868 .interpreter
869 .global_callable_ty(&callable)
870 .ok_or(QSharpError::new_err("callable not found"))?;
871 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
872 CircuitEntryPoint::Callable(callable, args)
873 }
874 _ => {
875 return Err(PyException::new_err(
876 "either entry_expr or operation must be specified",
877 ));
878 }
879 };
880
881 let tracer_config = qsc::circuit::TracerConfig {
882 max_operations: config
883 .max_operations
884 .unwrap_or(TracerConfig::DEFAULT_MAX_OPERATIONS),
885 source_locations: config.source_locations,
886 group_by_scope: config.group_by_scope,
887 prune_classical_qubits: config.prune_classical_qubits,
888 };
889
890 let generation_method = if let Some(generation_method) = config.generation_method {
891 generation_method.into()
892 } else {
893 qsc::interpret::CircuitGenerationMethod::ClassicalEval
894 };
895
896 match self
897 .interpreter
898 .circuit(entrypoint, generation_method, tracer_config)
899 {
900 Ok(circuit) => Circuit(circuit).into_py_any(py),
901 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
902 }
903 }
904
905 #[pyo3(signature=(job_params, entry_expr=None, callable=None, args=None))]
906 fn estimate(
907 &mut self,
908 py: Python,
909 job_params: &str,
910 entry_expr: Option<&str>,
911 callable: Option<Py<PyAny>>,
912 args: Option<Py<PyAny>>,
913 ) -> PyResult<String> {
914 let results = if let Some(entry_expr) = entry_expr {
915 estimate_expr(&mut self.interpreter, entry_expr, job_params)
916 } else {
917 let callable = callable.ok_or_else(|| {
918 QSharpError::new_err("either entry_expr or callable must be specified")
919 })?;
920 let callable = extract_callable_value(py, &callable)?;
921 let (input_ty, output_ty) = self
922 .interpreter
923 .global_callable_ty(&callable)
924 .ok_or(QSharpError::new_err("callable not found"))?;
925 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
926 estimate_call(&mut self.interpreter, callable, args, job_params)
927 };
928 match results {
929 Ok(estimate) => Ok(estimate),
930 Err(errors) if matches!(errors[0], re::Error::Interpreter(_)) => {
931 Err(QSharpError::new_err(format_errors(
932 errors
933 .into_iter()
934 .map(|e| match e {
935 re::Error::Interpreter(e) => e,
936 re::Error::Estimation(_) => unreachable!(),
937 })
938 .collect::<Vec<_>>(),
939 )))
940 }
941 Err(errors) => Err(QSharpError::new_err(
942 errors
943 .into_iter()
944 .map(|e| match e {
945 re::Error::Estimation(e) => e.to_string(),
946 re::Error::Interpreter(_) => unreachable!(),
947 })
948 .collect::<Vec<_>>()
949 .join("\n"),
950 )),
951 }
952 }
953
954 #[pyo3(signature=(entry_expr=None, callable=None, args=None))]
955 fn logical_counts<'a>(
956 &mut self,
957 py: Python<'a>,
958 entry_expr: Option<&str>,
959 callable: Option<Py<PyAny>>,
960 args: Option<Py<PyAny>>,
961 ) -> PyResult<Bound<'a, PyDict>> {
962 let results = if let Some(entry_expr) = entry_expr {
963 logical_counts_expr(&mut self.interpreter, entry_expr)
964 } else {
965 let callable = callable.ok_or_else(|| {
966 QSharpError::new_err("either entry_expr or callable must be specified")
967 })?;
968 let callable = extract_callable_value(py, &callable)?;
969 let (input_ty, output_ty) = self
970 .interpreter
971 .global_callable_ty(&callable)
972 .ok_or(QSharpError::new_err("callable not found"))?;
973 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
974 logical_counts_call(&mut self.interpreter, callable, args)
975 };
976 match results {
977 Ok(counts) => {
978 let dict = PyDict::new(py);
979 dict.set_item("numQubits", counts.num_qubits)?;
980 dict.set_item("tCount", counts.t_count)?;
981 dict.set_item("rotationCount", counts.rotation_count)?;
982 dict.set_item("rotationDepth", counts.rotation_depth)?;
983 dict.set_item("cczCount", counts.ccz_count)?;
984 dict.set_item("ccixCount", counts.ccix_count)?;
985 dict.set_item("measurementCount", counts.measurement_count)?;
986 if let Some(num_compute_qubits) = counts.num_compute_qubits {
987 dict.set_item("numComputeQubits", num_compute_qubits)?;
988 }
989 if let Some(read_from_memory_count) = counts.read_from_memory_count {
990 dict.set_item("readFromMemoryCount", read_from_memory_count)?;
991 }
992 if let Some(write_to_memory_count) = counts.write_to_memory_count {
993 dict.set_item("writeToMemoryCount", write_to_memory_count)?;
994 }
995 Ok(dict)
996 }
997 Err(errors) if matches!(errors[0], re::Error::Interpreter(_)) => {
998 Err(QSharpError::new_err(format_errors(
999 errors
1000 .into_iter()
1001 .map(|e| match e {
1002 re::Error::Interpreter(e) => e,
1003 re::Error::Estimation(_) => unreachable!(),
1004 })
1005 .collect::<Vec<_>>(),
1006 )))
1007 }
1008 Err(errors) => Err(QSharpError::new_err(
1009 errors
1010 .into_iter()
1011 .map(|e| match e {
1012 re::Error::Estimation(e) => e.to_string(),
1013 re::Error::Interpreter(_) => unreachable!(),
1014 })
1015 .collect::<Vec<_>>()
1016 .join("\n"),
1017 )),
1018 }
1019 }
1020}
1021
1022fn args_to_values(
1023 ctx: &interpret::Interpreter,
1024 py: Python,
1025 args: Option<Py<PyAny>>,
1026 input_ty: &Ty,
1027 output_ty: &Ty,
1028) -> PyResult<Value> {
1029 // If the types are not supported, we can't convert the arguments or return value.
1030 // Check this before trying to convert the arguments, and return an error if the types are not supported.
1031 if let Some(ty) = first_unsupported_interop_ty(ctx, input_ty) {
1032 return Err(QSharpError::new_err(format!(
1033 "unsupported input type: `{ty}`"
1034 )));
1035 }
1036 if let Some(ty) = first_unsupported_interop_ty(ctx, output_ty) {
1037 return Err(QSharpError::new_err(format!(
1038 "unsupported output type: `{ty}`"
1039 )));
1040 }
1041
1042 // Convert the Python arguments to Q# values, treating None as an empty tuple aka `Unit`.
1043 if matches!(&input_ty, Ty::Tuple(tup) if tup.is_empty()) {
1044 // Special case for unit, where args should be None
1045 if args.is_some() {
1046 return Err(QSharpError::new_err("expected no arguments"));
1047 }
1048 Ok(Value::unit())
1049 } else {
1050 let Some(args) = args else {
1051 return Err(QSharpError::new_err(format!(
1052 "expected arguments of type `{input_ty}`"
1053 )));
1054 };
1055 // This conversion will produce errors if the types don't match or can't be converted.
1056 Ok(pyobj_to_value(ctx, py, &args, input_ty)?)
1057 }
1058}
1059
1060/// Finds any Q# type recursively that does not support interop with Python, meaning our code cannot convert it back and forth
1061/// across the interop boundary.
1062fn first_unsupported_interop_ty<'ctx, 'ty>(
1063 ctx: &'ctx interpret::Interpreter,
1064 ty: &'ty Ty,
1065) -> Option<&'ctx Ty>
1066where
1067 'ty: 'ctx,
1068{
1069 match ty {
1070 Ty::Prim(prim_ty) => match prim_ty {
1071 Prim::Pauli
1072 | Prim::BigInt
1073 | Prim::Bool
1074 | Prim::Double
1075 | Prim::Int
1076 | Prim::String
1077 | Prim::Result => None,
1078 Prim::Qubit | Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => {
1079 Some(ty)
1080 }
1081 },
1082 Ty::Tuple(tup) => tup
1083 .iter()
1084 .find(|t| first_unsupported_interop_ty(ctx, t).is_some()),
1085 Ty::Array(ty) => first_unsupported_interop_ty(ctx, ty),
1086 Ty::Udt(_, res) => {
1087 let qsc::hir::Res::Item(item_id) = res else {
1088 panic!("Udt should be an item");
1089 };
1090 let (udt, _) = ctx.udt_ty_from_item_id(item_id);
1091
1092 let Ok(fields) = collect_udt_fields(udt) else {
1093 return Some(ty);
1094 };
1095
1096 for field in fields {
1097 if let Some(ty) = first_unsupported_interop_ty(ctx, field.1) {
1098 return Some(ty);
1099 }
1100 }
1101
1102 None
1103 }
1104 Ty::Arrow(..) => None,
1105 Ty::Infer(..) | Ty::Param { .. } | Ty::Err => Some(ty),
1106 }
1107}
1108
1109fn extract_callable_value(py: Python, callable: &Py<PyAny>) -> PyResult<Value> {
1110 if let Ok(global_callable) = callable.extract::<GlobalCallable>(py) {
1111 Ok(global_callable.0)
1112 } else if let Ok(closure) = callable.extract::<Closure>(py) {
1113 Ok(closure.0)
1114 } else {
1115 Err(PyException::new_err(
1116 "callable must be either a GlobalCallable or a Closure",
1117 ))
1118 }
1119}
1120
1121#[pyfunction]
1122pub fn physical_estimates(logical_resources: &str, job_params: &str) -> PyResult<String> {
1123 match re::estimate_physical_resources_from_json(logical_resources, job_params) {
1124 Ok(estimates) => Ok(estimates),
1125 Err(error) => Err(QSharpError::new_err(error.to_string())),
1126 }
1127}
1128
1129create_exception!(
1130 module,
1131 QSharpError,
1132 pyo3::exceptions::PyException,
1133 "An error returned from the Q# interpreter."
1134);
1135
1136create_exception!(
1137 module,
1138 QasmError,
1139 pyo3::exceptions::PyException,
1140 "An error returned from the OpenQASM parser."
1141);
1142
1143pub(crate) fn format_errors(errors: Vec<interpret::Error>) -> String {
1144 errors
1145 .into_iter()
1146 .map(|e| format_error(&e))
1147 .collect::<Vec<_>>()
1148 .join("\n")
1149}
1150
1151pub(crate) fn format_error(e: &interpret::Error) -> String {
1152 let mut message = String::new();
1153 if let Some(stack_trace) = e.stack_trace() {
1154 write!(message, "{stack_trace}").expect("write should succeed");
1155 }
1156 let additional_help = python_help(e);
1157 let report = Report::new(e.clone());
1158 write!(message, "{report:?}")
1159 .unwrap_or_else(|err| panic!("writing error failed: {err} error was: {e:?}"));
1160 if let Some(additional_help) = additional_help {
1161 writeln!(message, "{additional_help}").expect("write should succeed");
1162 }
1163 message
1164}
1165
1166/// Additional help text for an error specific to the Python module
1167fn python_help(error: &interpret::Error) -> Option<String> {
1168 if matches!(error, interpret::Error::UnsupportedRuntimeCapabilities) {
1169 Some("Unsupported target profile. Initialize Q# by running `qsharp.init(target_profile=qsharp.TargetProfile.Base)` before performing code generation.".into())
1170 } else {
1171 None
1172 }
1173}
1174
1175#[pyclass]
1176pub(crate) struct Output(DisplayableOutput);
1177
1178#[pymethods]
1179/// An output returned from the Q# interpreter.
1180/// Outputs can be a state dumps or messages. These are normally printed to the console.
1181impl Output {
1182 fn __repr__(&self) -> String {
1183 match &self.0 {
1184 DisplayableOutput::State(state) => state.to_plain(),
1185 DisplayableOutput::Matrix(matrix) => matrix.to_plain(),
1186 DisplayableOutput::Message(msg) => msg.clone(),
1187 }
1188 }
1189
1190 fn __str__(&self) -> String {
1191 self.__repr__()
1192 }
1193
1194 fn _repr_markdown_(&self) -> Option<String> {
1195 match &self.0 {
1196 DisplayableOutput::State(state) => {
1197 let latex = if let Some(latex) = state.to_latex() {
1198 format!("\n\n{latex}")
1199 } else {
1200 String::default()
1201 };
1202 Some(format!("{}{latex}", state.to_html()))
1203 }
1204 DisplayableOutput::Message(_) => None,
1205 DisplayableOutput::Matrix(matrix) => Some(matrix.to_latex()),
1206 }
1207 }
1208
1209 fn state_dump(&self) -> Option<StateDumpData> {
1210 match &self.0 {
1211 DisplayableOutput::State(state) => Some(StateDumpData(state.clone())),
1212 DisplayableOutput::Matrix(_) | DisplayableOutput::Message(_) => None,
1213 }
1214 }
1215
1216 fn is_state_dump(&self) -> bool {
1217 matches!(&self.0, DisplayableOutput::State(_))
1218 }
1219
1220 fn is_matrix(&self) -> bool {
1221 matches!(&self.0, DisplayableOutput::Matrix(_))
1222 }
1223
1224 fn is_message(&self) -> bool {
1225 matches!(&self.0, DisplayableOutput::Message(_))
1226 }
1227}
1228
1229#[pyclass]
1230/// Captured simulation state dump.
1231pub(crate) struct StateDumpData(pub(crate) DisplayableState);
1232
1233#[pymethods]
1234impl StateDumpData {
1235 fn get_dict<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyDict>> {
1236 let dict = rustc_hash::FxHashMap::from_iter(self.0.0.clone());
1237 dict.into_pyobject(py)
1238 }
1239
1240 #[getter]
1241 fn get_qubit_count(&self) -> usize {
1242 self.0.1
1243 }
1244
1245 fn __len__(&self) -> usize {
1246 self.0.0.len()
1247 }
1248
1249 fn __repr__(&self) -> String {
1250 self.0.to_plain()
1251 }
1252
1253 fn __str__(&self) -> String {
1254 self.__repr__()
1255 }
1256
1257 fn _repr_markdown_(&self) -> String {
1258 let latex = if let Some(latex) = self.0.to_latex() {
1259 format!("\n\n{latex}")
1260 } else {
1261 String::default()
1262 };
1263 format!("{}{latex}", self.0.to_html())
1264 }
1265
1266 fn _repr_latex_(&self) -> Option<String> {
1267 self.0.to_latex()
1268 }
1269}
1270
1271#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1272#[pyclass(eq, eq_int, from_py_object, ord)]
1273/// A Q# measurement result.
1274pub(crate) enum Result {
1275 Zero,
1276 One,
1277 Loss,
1278}
1279
1280impl From<Result> for qsc::interpret::Result {
1281 fn from(value: Result) -> Self {
1282 match value {
1283 Result::Loss => qsc::interpret::Result::Loss,
1284 Result::One | Result::Zero => qsc::interpret::Result::Val(value == Result::One),
1285 }
1286 }
1287}
1288
1289#[pymethods]
1290impl Result {
1291 #[allow(clippy::trivially_copy_pass_by_ref)]
1292 fn __repr__(&self) -> String {
1293 match self {
1294 Result::Zero => "Zero".to_owned(),
1295 Result::One => "One".to_owned(),
1296 Result::Loss => "Loss".to_owned(),
1297 }
1298 }
1299
1300 #[allow(clippy::trivially_copy_pass_by_ref)]
1301 fn __str__(&self) -> String {
1302 self.__repr__()
1303 }
1304
1305 #[allow(clippy::trivially_copy_pass_by_ref)]
1306 fn __hash__(&self) -> u32 {
1307 match self {
1308 Result::Zero => 0,
1309 Result::One => 1,
1310 Result::Loss => 2,
1311 }
1312 }
1313}
1314
1315#[derive(Clone, Copy, PartialEq)]
1316#[pyclass(eq, eq_int, from_py_object)]
1317/// A Q# Pauli operator.
1318pub(crate) enum Pauli {
1319 I,
1320 X,
1321 Y,
1322 Z,
1323}
1324
1325impl From<Pauli> for fir::Pauli {
1326 fn from(value: Pauli) -> Self {
1327 match value {
1328 Pauli::I => fir::Pauli::I,
1329 Pauli::X => fir::Pauli::X,
1330 Pauli::Y => fir::Pauli::Y,
1331 Pauli::Z => fir::Pauli::Z,
1332 }
1333 }
1334}
1335
1336pub(crate) struct OptionalCallbackReceiver<'a> {
1337 pub(crate) callback: Option<Py<PyAny>>,
1338 pub(crate) py: Python<'a>,
1339}
1340
1341impl Receiver for OptionalCallbackReceiver<'_> {
1342 fn state(
1343 &mut self,
1344 state: Vec<(BigUint, Complex64)>,
1345 qubit_count: usize,
1346 ) -> core::result::Result<(), Error> {
1347 if let Some(callback) = &self.callback {
1348 let out = DisplayableOutput::State(DisplayableState(state, qubit_count));
1349 callback
1350 .call1(
1351 self.py,
1352 PyTuple::new(
1353 self.py,
1354 &[Py::new(self.py, Output(out)).expect("should be able to create output")],
1355 )
1356 .map_err(|_| Error)?,
1357 )
1358 .map_err(|_| Error)?;
1359 }
1360 Ok(())
1361 }
1362
1363 fn matrix(&mut self, matrix: Vec<Vec<Complex64>>) -> std::result::Result<(), Error> {
1364 if let Some(callback) = &self.callback {
1365 let out = DisplayableOutput::Matrix(DisplayableMatrix(matrix));
1366 callback
1367 .call1(
1368 self.py,
1369 PyTuple::new(
1370 self.py,
1371 &[Py::new(self.py, Output(out)).expect("should be able to create output")],
1372 )
1373 .map_err(|_| Error)?,
1374 )
1375 .map_err(|_| Error)?;
1376 }
1377 Ok(())
1378 }
1379
1380 fn message(&mut self, msg: &str) -> core::result::Result<(), Error> {
1381 if let Some(callback) = &self.callback {
1382 let out = DisplayableOutput::Message(msg.to_owned());
1383 callback
1384 .call1(
1385 self.py,
1386 PyTuple::new(
1387 self.py,
1388 &[Py::new(self.py, Output(out)).expect("should be able to create output")],
1389 )
1390 .map_err(|_| Error)?,
1391 )
1392 .map_err(|_| Error)?;
1393 }
1394 Ok(())
1395 }
1396}
1397
1398#[pyclass]
1399pub(crate) struct CircuitConfig {
1400 #[pyo3(get, set)]
1401 pub(crate) max_operations: Option<usize>,
1402 #[pyo3(get, set)]
1403 pub(crate) generation_method: Option<CircuitGenerationMethod>,
1404 #[pyo3(get, set)]
1405 pub(crate) source_locations: bool,
1406 #[pyo3(get, set)]
1407 pub(crate) group_by_scope: bool,
1408 #[pyo3(get, set)]
1409 pub(crate) prune_classical_qubits: bool,
1410}
1411
1412#[pymethods]
1413impl CircuitConfig {
1414 #[new]
1415 #[pyo3(signature=(*,max_operations=None, generation_method=None, source_locations=false, group_by_scope=false, prune_classical_qubits=false))]
1416 fn new(
1417 max_operations: Option<usize>,
1418 generation_method: Option<CircuitGenerationMethod>,
1419 source_locations: bool,
1420 group_by_scope: bool,
1421 prune_classical_qubits: bool,
1422 ) -> Self {
1423 Self {
1424 max_operations,
1425 generation_method,
1426 source_locations,
1427 group_by_scope,
1428 prune_classical_qubits,
1429 }
1430 }
1431}
1432
1433#[pyclass(from_py_object)]
1434#[derive(Clone, Copy)]
1435pub(crate) enum CircuitGenerationMethod {
1436 ClassicalEval,
1437 Simulate,
1438 Static,
1439}
1440
1441impl From<CircuitGenerationMethod> for qsc::interpret::CircuitGenerationMethod {
1442 fn from(value: CircuitGenerationMethod) -> Self {
1443 match value {
1444 CircuitGenerationMethod::ClassicalEval => {
1445 qsc::interpret::CircuitGenerationMethod::ClassicalEval
1446 }
1447 CircuitGenerationMethod::Simulate => qsc::interpret::CircuitGenerationMethod::Simulate,
1448 CircuitGenerationMethod::Static => qsc::interpret::CircuitGenerationMethod::Static,
1449 }
1450 }
1451}
1452
1453#[pyclass]
1454pub(crate) struct Circuit(pub qsc::circuit::Circuit);
1455
1456#[pymethods]
1457impl Circuit {
1458 fn __repr__(&self) -> String {
1459 // Disable rendering source locations for user-facing string representation,
1460 // as they make the circuit look cluttered.
1461 self.0.display_no_locations().to_string()
1462 }
1463
1464 fn __str__(&self) -> String {
1465 self.__repr__()
1466 }
1467
1468 fn json(&self, _py: Python) -> PyResult<String> {
1469 serde_json::to_string(&self.0).map_err(|e| PyException::new_err(e.to_string()))
1470 }
1471}
1472
1473trait IntoPyErr {
1474 fn into_py_err(self) -> PyErr;
1475}
1476
1477impl IntoPyErr for Report {
1478 fn into_py_err(self) -> PyErr {
1479 PyException::new_err(format!("{self:?}"))
1480 }
1481}
1482
1483impl<E> IntoPyErr for Vec<E>
1484where
1485 E: Diagnostic + Send + Sync + 'static,
1486{
1487 fn into_py_err(self) -> PyErr {
1488 let mut message = String::new();
1489 for diag in self {
1490 let report = Report::new(diag);
1491 writeln!(message, "{report:?}").expect("string should be writable");
1492 }
1493 PyException::new_err(message)
1494 }
1495}
1496
1497#[pyclass(from_py_object, unsendable)]
1498#[derive(Clone)]
1499struct GlobalCallable(Value);
1500
1501impl From<Value> for GlobalCallable {
1502 fn from(val: Value) -> Self {
1503 match val {
1504 val @ Value::Global(..) => GlobalCallable(val),
1505 _ => panic!("expected global callable"),
1506 }
1507 }
1508}
1509
1510impl From<GlobalCallable> for Value {
1511 fn from(val: GlobalCallable) -> Self {
1512 val.0
1513 }
1514}
1515
1516#[pyclass(from_py_object, unsendable)]
1517#[derive(Clone)]
1518struct Closure(Value);
1519
1520impl From<Value> for Closure {
1521 fn from(val: Value) -> Self {
1522 match val {
1523 val @ Value::Closure(..) => Closure(val),
1524 _ => panic!("expected closure"),
1525 }
1526 }
1527}
1528
1529impl From<Closure> for Value {
1530 fn from(val: Closure) -> Self {
1531 val.0
1532 }
1533}
1534
1535/// Create a Python callable from a Q# callable and adds it to the given environment.
1536fn create_py_callable(
1537 py: Python,
1538 make_callable: &Py<PyAny>,
1539 namespace: &[Rc<str>],
1540 name: &str,
1541 val: Value,
1542) -> PyResult<()> {
1543 if namespace.is_empty() && name == "<lambda>" {
1544 // We don't want to bind auto-generated lambda callables.
1545 return Ok(());
1546 }
1547
1548 let args = (
1549 Py::new(py, GlobalCallable::from(val)).expect("should be able to create callable"), // callable id
1550 PyList::new(py, namespace.iter().map(ToString::to_string))?, // namespace as string array
1551 PyString::new(py, name), // name of callable
1552 );
1553
1554 // Call into the Python layer to create the function wrapping the callable invocation.
1555 make_callable.call1(py, args)?;
1556
1557 Ok(())
1558}
1559
1560/// Create a Python class from a Q# type and adds it to the given environment.
1561fn create_py_class(
1562 ctx: &interpret::Interpreter,
1563 py: Python,
1564 make_class: &Py<PyAny>,
1565 namespace: &[Rc<str>],
1566 name: &str,
1567 ty: &Ty,
1568) -> PyResult<()> {
1569 let Some(type_ir) = type_ir_from_qsharp_ty(ctx, ty) else {
1570 // If the UDT can't be expressed in Python, we don't want to raise
1571 // an error, instead we just don't define that type in `qsharp.code.*`.
1572 return Ok(());
1573 };
1574
1575 let args = (
1576 Py::new(py, type_ir).expect("should be able to create callable"), // callable id
1577 PyList::new(py, namespace.iter().map(ToString::to_string))?, // namespace as string array
1578 PyString::new(py, name), // name of callable
1579 );
1580
1581 // Call into the Python layer to create the function wrapping the callable invocation.
1582 make_class.call1(py, args)?;
1583
1584 Ok(())
1585}