microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
brlackey/neutral-atom-models

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/interop.rs

892lines · 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
9use std::fmt::Write;
10use std::path::PathBuf;
11use std::str::FromStr;
12use std::sync::Arc;
13
14use pyo3::IntoPyObjectExt;
15use pyo3::exceptions::PyException;
16use pyo3::prelude::*;
17use pyo3::types::{PyDict, PyList};
18use qsc::circuit::TracerConfig;
19use qsc::hir::PackageId;
20use qsc::interpret::output::Receiver;
21use qsc::interpret::{CircuitEntryPoint, Interpreter, SimType, into_errors};
22use qsc::openqasm::compiler::compile_to_qsharp_ast_with_config;
23use qsc::openqasm::semantic::QasmSemanticParseResult;
24use qsc::openqasm::{OperationSignature, QubitSemantics};
25use qsc::project::ProjectType;
26use qsc::target::Profile;
27use qsc::{Backend, CliffordSim, PackageType, PauliNoise, SparseSim};
28use qsc::{
29 LanguageFeatures, SourceMap, ast::Package, error::WithSource, interpret, project::FileSystem,
30};
31
32use crate::fs::file_system;
33use crate::interpreter::data_interop::value_to_pyobj;
34use crate::interpreter::{
35 CircuitConfig, OptionalCallbackReceiver, OutputSemantics, ProgramType, QSharpError, QasmError,
36 TargetProfile, format_error, format_errors,
37};
38use crate::qir_simulation::{NoiseConfig, unbind_noise_config};
39
40use resource_estimator as re;
41
42/// Runs the given OpenQASM program for the given number of shots.
43/// Each shot uses an independent instance of the simulator.
44///
45/// Note:
46/// This call while exported is not intended to be used directly by the user.
47/// It is intended to be used by the Python wrapper which will handle the
48/// callbacks and other Python specific details.
49///
50/// Args:
51/// source (str): The OpenQASM source code to execute.
52/// output_fn (Callable[[Output], None]): The function to handle the output of the execution.
53/// noise: The noise to use in simulation.
54/// read_file (Callable[[str], Tuple[str, str]]): The function to read a file and return its contents.
55/// list_directory (Callable[[str], List[Dict[str, str]]]): The function to list the contents of a directory.
56/// resolve_path (Callable[[str, str], str]): The function to resolve a path given a base path and a relative path.
57/// fetch_github (Callable[[str, str, str, str], str]): The function to fetch a file from GitHub.
58/// **kwargs: Additional keyword arguments to pass to the execution.
59/// - target_profile (TargetProfile): The target profile to use for execution.
60/// - name (str): The name of the circuit. This is used as the entry point for the program. Defaults to 'program'.
61/// - search_path (str): The optional search path for resolving imports.
62/// - output_semantics (OutputSemantics, optional): The output semantics for the compilation.
63/// - shots (int): The number of shots to run the program for. Defaults to 1.
64/// - seed (int): The seed to use for the random number generator.
65///
66/// Returns:
67/// Any: The result of the execution.
68///
69/// Raises:
70/// QasmError: If there is an error generating, parsing, or analyzing the OpenQASM source.
71/// QSharpError: If there is an error interpreting the input.
72#[pyfunction]
73#[allow(clippy::too_many_arguments)]
74#[pyo3(
75 signature = (source, callback=None, noise_config=None,noise=None, qubit_loss=None, read_file=None, list_directory=None, resolve_path=None, fetch_github=None, **kwargs)
76)]
77pub(crate) fn run_qasm_program(
78 py: Python,
79 source: &str,
80 callback: Option<Py<PyAny>>,
81 noise_config: Option<&Bound<NoiseConfig>>,
82 noise: Option<(f64, f64, f64)>,
83 qubit_loss: Option<f64>,
84 read_file: Option<Py<PyAny>>,
85 list_directory: Option<Py<PyAny>>,
86 resolve_path: Option<Py<PyAny>>,
87 fetch_github: Option<Py<PyAny>>,
88 kwargs: Option<Bound<'_, PyDict>>,
89) -> PyResult<Py<PyAny>> {
90 let mut receiver = OptionalCallbackReceiver { callback, py };
91
92 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
93
94 let target = get_target_profile(&kwargs)?;
95 let operation_name = get_operation_name(&kwargs)?;
96 let output_semantics = get_output_semantics(&kwargs, || OutputSemantics::OpenQasm)?;
97 let seed = get_seed(&kwargs);
98 let shots = get_shots(&kwargs)?;
99 let search_path = get_search_path(&kwargs)?;
100 let sim_type = get_sim_type(&kwargs)?;
101
102 let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
103 let file_path = PathBuf::from_str(&search_path)
104 .expect("from_str is infallible")
105 .join("program.qasm");
106 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(source)));
107 let ProjectType::OpenQASM(sources) = project.project_type else {
108 return Err(QasmError::new_err(
109 "Expected OpenQASM project, but got a different type".to_string(),
110 ));
111 };
112 let res = qsc::openqasm::semantic::parse_sources(&sources);
113 let (package, source_map, signature) = compile_qasm_enriching_errors(
114 res,
115 &operation_name,
116 ProgramType::File,
117 output_semantics,
118 false,
119 )?;
120
121 let package_type = PackageType::Exe;
122 let language_features = LanguageFeatures::default();
123 let mut interpreter =
124 create_interpreter_from_ast(package, source_map, target, language_features, package_type)
125 .map_err(|errors| QSharpError::new_err(format_errors(errors)))?;
126
127 let entry_expr = signature.create_entry_expr_from_params(String::new());
128 interpreter
129 .set_entry_expr(&entry_expr)
130 .map_err(|errors| map_entry_compilation_errors(errors, &signature))?;
131
132 let noise = match noise {
133 None => None,
134 Some((px, py, pz)) => match PauliNoise::from_probabilities(px, py, pz) {
135 Ok(noise_struct) => Some(noise_struct),
136 Err(error_message) => return Err(PyException::new_err(error_message)),
137 },
138 };
139 let loss = qubit_loss.unwrap_or(0.0);
140 // Convert NoiseConfig to a rust NoiseConfig.
141 let noise_config: Option<qdk_simulators::noise_config::NoiseConfig<f64, f64>> =
142 noise_config.map(|noise_config| unbind_noise_config(py, noise_config));
143 let result = run_ast(
144 &mut interpreter,
145 &mut receiver,
146 shots,
147 seed,
148 noise_config.as_ref(),
149 noise,
150 loss,
151 sim_type,
152 );
153 match result {
154 Ok(result) => {
155 let list: Result<Vec<_>, _> = result
156 .iter()
157 .map(|v| value_to_pyobj(&interpreter, py, v))
158 .collect();
159 Ok(PyList::new(py, list?)?.into())
160 }
161 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
162 }
163}
164
165#[allow(clippy::too_many_arguments)]
166pub(crate) fn run_ast(
167 interpreter: &mut Interpreter,
168 receiver: &mut impl Receiver,
169 shots: usize,
170 seed: Option<u64>,
171 noise_config: Option<&qdk_simulators::noise_config::NoiseConfig<f64, f64>>,
172 noise: Option<PauliNoise>,
173 loss: f64,
174 sim_type: SimType,
175) -> Result<Vec<qsc::interpret::Value>, Vec<interpret::Error>> {
176 let mut results = Vec::with_capacity(shots);
177 for i in 0..shots {
178 let result = match sim_type {
179 SimType::Sparse => {
180 let mut sim = if let Some(noise) = noise {
181 SparseSim::new_with_noise(&noise)
182 } else {
183 match noise_config {
184 Some(noise_config) => {
185 SparseSim::new_with_noise_config(noise_config.clone().into())
186 }
187 None => SparseSim::new(),
188 }
189 };
190 if loss > 0.0 {
191 sim.set_loss(loss);
192 }
193 // If seed is provided, we want to use a different seed for each shot
194 // so that the results are different for each shot, but still deterministic
195 sim.set_seed(seed.map(|s| s + i as u64));
196 interpreter.run_with_sim(&mut sim, receiver, None, None)?
197 }
198 SimType::Clifford(num_qubits) => {
199 let mut sim = match noise_config {
200 None => CliffordSim::new(num_qubits),
201 Some(noise_config) => {
202 CliffordSim::new_with_noise_config(num_qubits, noise_config.clone().into())
203 }
204 };
205 // If seed is provided, we want to use a different seed for each shot
206 // so that the results are different for each shot, but still deterministic
207 sim.set_seed(seed.map(|s| s + i as u64));
208 interpreter.run_with_sim(&mut sim, receiver, None, None)?
209 }
210 };
211
212 results.push(result);
213 }
214
215 Ok(results)
216}
217
218/// Estimates the resource requirements for executing OpenQASM source code.
219///
220/// Note:
221/// This call while exported is not intended to be used directly by the user.
222/// It is intended to be used by the Python wrapper which will handle the
223/// callbacks and other Python specific details.
224///
225/// Args:
226/// source (str): The OpenQASM source code to estimate the resource requirements for.
227/// job_params (str): The parameters for the job.
228/// read_file (Callable[[str], Tuple[str, str]]): A callable that reads a file and returns its content and path.
229/// list_directory (Callable[[str], List[Dict[str, str]]]): A callable that lists the contents of a directory.
230/// resolve_path (Callable[[str, str], str]): A callable that resolves a file path given a base path and a relative path.
231/// fetch_github (Callable[[str, str, str, str], str]): A callable that fetches a file from GitHub.
232/// **kwargs: Additional keyword arguments to pass to the execution.
233/// - name (str): The name of the circuit. This is used as the entry point for the program. Defaults to 'program'.
234/// - search_path (str): The optional search path for resolving imports.
235/// Returns:
236/// str: The estimated resource requirements for executing the OpenQASM source code.
237#[pyfunction]
238#[allow(clippy::too_many_arguments)]
239#[pyo3(
240 signature = (source, job_params, read_file, list_directory, resolve_path, fetch_github, **kwargs)
241)]
242pub(crate) fn resource_estimate_qasm_program(
243 py: Python,
244 source: &str,
245 job_params: &str,
246 read_file: Option<Py<PyAny>>,
247 list_directory: Option<Py<PyAny>>,
248 resolve_path: Option<Py<PyAny>>,
249 fetch_github: Option<Py<PyAny>>,
250 kwargs: Option<Bound<'_, PyDict>>,
251) -> PyResult<String> {
252 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
253
254 let operation_name = get_operation_name(&kwargs)?;
255 let search_path = get_search_path(&kwargs)?;
256
257 let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
258 let file_path = PathBuf::from_str(&search_path)
259 .expect("from_str is infallible")
260 .join("program.qasm");
261 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(source)));
262 let ProjectType::OpenQASM(sources) = project.project_type else {
263 return Err(QasmError::new_err(
264 "Expected OpenQASM project, but got a different type".to_string(),
265 ));
266 };
267 let res = qsc::openqasm::semantic::parse_sources(&sources);
268
269 let program_type = ProgramType::File;
270 let output_semantics = OutputSemantics::ResourceEstimation;
271 let (package, source_map, _) =
272 compile_qasm_enriching_errors(res, &operation_name, program_type, output_semantics, false)?;
273
274 match crate::interop::estimate_qasm(package, source_map, job_params) {
275 Ok(estimate) => Ok(estimate),
276 Err(errors) if matches!(errors[0], re::Error::Interpreter(_)) => {
277 Err(QSharpError::new_err(format_errors(
278 errors
279 .into_iter()
280 .map(|e| match e {
281 re::Error::Interpreter(e) => e,
282 re::Error::Estimation(_) => unreachable!(),
283 })
284 .collect::<Vec<_>>(),
285 )))
286 }
287 Err(errors) => Err(QSharpError::new_err(
288 errors
289 .into_iter()
290 .map(|e| match e {
291 re::Error::Estimation(e) => e.to_string(),
292 re::Error::Interpreter(_) => unreachable!(),
293 })
294 .collect::<Vec<_>>()
295 .join("\n"),
296 )),
297 }
298}
299
300/// Compiles the OpenQASM source code into a program that can be submitted to a
301/// target as QIR (Quantum Intermediate Representation).
302///
303/// Note:
304/// This call while exported is not intended to be used directly by the user.
305/// It is intended to be used by the Python wrapper which will handle the
306/// callbacks and other Python specific details.
307///
308/// Args:
309/// source (str): The OpenQASM source code to estimate the resource requirements for.
310/// read_file (Callable[[str], Tuple[str, str]]): A callable that reads a file and returns its content and path.
311/// list_directory (Callable[[str], List[Dict[str, str]]]): A callable that lists the contents of a directory.
312/// resolve_path (Callable[[str, str], str]): A callable that resolves a file path given a base path and a relative path.
313/// fetch_github (Callable[[str, str, str, str], str]): A callable that fetches a file from GitHub.
314/// **kwargs: Additional keyword arguments to pass to the compilation when source program is provided.
315/// - name (str): The name of the circuit. This is used as the entry point for the program.
316/// - target_profile (TargetProfile): The target profile to use for code generation.
317/// - search_path (Optional[str]): The optional search path for resolving file references.
318/// - output_semantics (OutputSemantics, optional): The output semantics for the compilation.
319///
320/// Returns:
321/// str: The converted QIR code as a string.
322///
323/// Raises:
324/// QasmError: If there is an error generating, parsing, or analyzing the OpenQASM source.
325/// QSharpError: If there is an error compiling the program.
326#[pyfunction]
327#[allow(clippy::too_many_arguments)]
328#[pyo3(
329 signature = (source, read_file, list_directory, resolve_path, fetch_github, **kwargs)
330)]
331pub(crate) fn compile_qasm_program_to_qir(
332 py: Python,
333 source: &str,
334 read_file: Option<Py<PyAny>>,
335 list_directory: Option<Py<PyAny>>,
336 resolve_path: Option<Py<PyAny>>,
337 fetch_github: Option<Py<PyAny>>,
338 kwargs: Option<Bound<'_, PyDict>>,
339) -> PyResult<String> {
340 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
341
342 let target = get_target_profile(&kwargs)?;
343 let operation_name = get_operation_name(&kwargs)?;
344 let search_path = get_search_path(&kwargs)?;
345
346 let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
347 let file_path = PathBuf::from_str(&search_path)
348 .expect("from_str is infallible")
349 .join("program.qasm");
350 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(source)));
351 let ProjectType::OpenQASM(sources) = project.project_type else {
352 return Err(QasmError::new_err(
353 "Expected OpenQASM project, but got a different type".to_string(),
354 ));
355 };
356 let res = qsc::openqasm::semantic::parse_sources(&sources);
357
358 let program_ty = ProgramType::File;
359 let output_semantics = get_output_semantics(&kwargs, || OutputSemantics::OpenQasm)?;
360 let (package, source_map, signature) =
361 compile_qasm_enriching_errors(res, &operation_name, program_ty, output_semantics, false)?;
362
363 let package_type = PackageType::Lib;
364 let language_features = LanguageFeatures::default();
365 let mut interpreter =
366 create_interpreter_from_ast(package, source_map, target, language_features, package_type)
367 .map_err(|errors| QSharpError::new_err(format_errors(errors)))?;
368 let entry_expr = signature.create_entry_expr_from_params(String::new());
369
370 generate_qir_from_ast(entry_expr, &mut interpreter)
371}
372
373pub(crate) fn compile_qasm_enriching_errors<S: AsRef<str>>(
374 semantic_parse_result: QasmSemanticParseResult,
375 operation_name: S,
376 program_ty: ProgramType,
377 output_semantics: OutputSemantics,
378 allow_input_params: bool,
379) -> PyResult<(Package, SourceMap, OperationSignature)> {
380 let config = qsc::openqasm::CompilerConfig::new(
381 QubitSemantics::Qiskit,
382 output_semantics.into(),
383 program_ty.into(),
384 Some(operation_name.as_ref().into()),
385 None,
386 );
387
388 let unit = compile_to_qsharp_ast_with_config(semantic_parse_result, config);
389
390 let (source_map, errors, package, sig, _) = unit.into_tuple();
391 if !errors.is_empty() {
392 return Err(QasmError::new_err(format_qasm_errors(errors)));
393 }
394
395 let Some(signature) = sig else {
396 return Err(QasmError::new_err(
397 "signature should have had value. This is a bug",
398 ));
399 };
400
401 if !signature.input.is_empty() && !allow_input_params {
402 // no entry expression is provided, but the signature has input parameters.
403 let mut message = String::new();
404 message += "Circuit has unbound input parameters\n";
405 write!(message, " help: Parameters: {}", signature.input_params())
406 .expect("writing to string should succeed");
407
408 return Err(QSharpError::new_err(message));
409 }
410
411 Ok((package, source_map, signature))
412}
413
414fn generate_qir_from_ast<S: AsRef<str>>(
415 entry_expr: S,
416 interpreter: &mut Interpreter,
417) -> PyResult<String> {
418 interpreter
419 .qirgen(entry_expr.as_ref())
420 .map_err(map_qirgen_errors)
421}
422
423/// This call while exported is not intended to be used directly by the user.
424/// It is intended to be used by the Python wrapper which will handle the
425/// callbacks and other Python specific details.
426#[pyfunction]
427#[allow(clippy::too_many_arguments)]
428#[pyo3(
429 signature = (source, read_file, list_directory, resolve_path, fetch_github, **kwargs)
430)]
431pub(crate) fn compile_qasm_to_qsharp(
432 py: Python,
433 source: &str,
434 read_file: Option<Py<PyAny>>,
435 list_directory: Option<Py<PyAny>>,
436 resolve_path: Option<Py<PyAny>>,
437 fetch_github: Option<Py<PyAny>>,
438 kwargs: Option<Bound<'_, PyDict>>,
439) -> PyResult<String> {
440 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
441
442 let operation_name = get_operation_name(&kwargs)?;
443 let search_path = get_search_path(&kwargs)?;
444
445 let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
446 let file_path = PathBuf::from_str(&search_path)
447 .expect("from_str is infallible")
448 .join("program.qasm");
449 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(source)));
450 let ProjectType::OpenQASM(sources) = project.project_type else {
451 return Err(QasmError::new_err(
452 "Expected OpenQASM project, but got a different type".to_string(),
453 ));
454 };
455 let res = qsc::openqasm::semantic::parse_sources(&sources);
456
457 let program_ty = get_program_type(&kwargs, || ProgramType::File)?;
458 let output_semantics = get_output_semantics(&kwargs, || OutputSemantics::OpenQasm)?;
459 let (package, _, _) =
460 compile_qasm_enriching_errors(res, &operation_name, program_ty, output_semantics, true)?;
461
462 let qsharp = qsc::codegen::qsharp::write_package_string(&package);
463 Ok(qsharp)
464}
465
466/// Enriches the compilation errors to provide more helpful messages
467/// as we know that we are compiling the entry expression.
468pub(crate) fn map_entry_compilation_errors(
469 errors: Vec<interpret::Error>,
470 sig: &OperationSignature,
471) -> PyErr {
472 let mut semantic = vec![];
473 for error in errors {
474 match &error {
475 interpret::Error::Compile(_) => {
476 // The entry expression is invalid. This is likely due to a type mismatch
477 // or missing parameter(s). We should provide a more helpful error message.
478 let mut message = format_error(&error);
479 writeln!(message).expect("write should succeed");
480 writeln!(message, "failed to compile entry point.").expect("write should succeed");
481 writeln!(
482 message,
483 " help: check that the parameter types match the supplied parameters"
484 )
485 .expect("write should succeed");
486
487 write!(message, " help: Parameters: {}", sig.input_params())
488 .expect("writing to string should succeed");
489
490 semantic.push(message);
491 }
492 _ => {
493 semantic.push(format_error(&error));
494 }
495 }
496 }
497 let message = semantic.into_iter().collect::<String>();
498 QSharpError::new_err(message)
499}
500
501/// Adds additional information to interpreter errors to make them more user-friendly.
502/// when QIR generation fails.
503fn map_qirgen_errors(errors: Vec<interpret::Error>) -> PyErr {
504 let mut semantic = vec![];
505 for error in errors {
506 match &error {
507 interpret::Error::Compile(_) => {
508 // We've gotten this far with no compilation errors, so if we get one here
509 // then the entry expression is invalid.
510 let mut message = format_error(&error);
511 writeln!(message).expect("write should succeed");
512 writeln!(message, "failed to compile entry point.").expect("write should succeed");
513 writeln!(
514 message,
515 " help: check that the parameter types match the entry point signature"
516 )
517 .expect("write should succeed");
518
519 semantic.push(message);
520 }
521 interpret::Error::PartialEvaluation(pe) => match pe.error() {
522 qsc::partial_eval::Error::OutputResultLiteral(..) => {
523 let mut message = format_error(&error);
524 writeln!(message).expect("write should succeed");
525 writeln!(
526 message,
527 " help: ensure all output registers have been measured into."
528 )
529 .expect("write should succeed");
530
531 semantic.push(message);
532 }
533 _ => {
534 semantic.push(format_error(&error));
535 }
536 },
537 _ => {
538 semantic.push(format_error(&error));
539 }
540 }
541 }
542 let message = semantic.into_iter().collect::<String>();
543 QSharpError::new_err(message)
544}
545
546/// Estimates the resources required to run a QASM program
547/// represented by the provided AST. The source map is used for
548/// error reporting during compilation or runtime.
549fn estimate_qasm(
550 ast_package: Package,
551 source_map: SourceMap,
552 params: &str,
553) -> Result<String, Vec<resource_estimator::Error>> {
554 let mut interpreter = create_interpreter_from_ast(
555 ast_package,
556 source_map,
557 Profile::Unrestricted,
558 LanguageFeatures::default(),
559 PackageType::Exe,
560 )
561 .map_err(into_estimation_errors)?;
562
563 resource_estimator::estimate_entry(&mut interpreter, params)
564}
565
566/// Synthesizes a circuit for an `OpenQASM` program.
567///
568/// Note:
569/// This call while exported is not intended to be used directly by the user.
570/// It is intended to be used by the Python wrapper which will handle the
571/// callbacks and other Python specific details.
572///
573/// Args:
574/// source (str): An `OpenQASM` program. Alternatively, a callable can be provided,
575/// which must be an already imported global callable.
576/// `read_file` (Callable[[str], Tuple[str, str]]): A callable that reads a file and returns its content and path.
577/// `list_directory` (Callable[[str], List[Dict[str, str]]]): A callable that lists the contents of a directory.
578/// `resolve_path` (Callable[[str, str], str]): A callable that resolves a file path given a base path and a relative path.
579/// `fetch_github` (Callable[[str, str, str, str], str]): A callable that fetches a file from GitHub.
580/// **kwargs: Additional keyword arguments to pass to the execution.
581/// - name (str): The name of the program. This is used as the entry point for the program.
582/// - `search_path` (Optional[str]): The optional search path for resolving file references.
583/// Returns:
584/// Circuit: The synthesized circuit.
585///
586/// Raises:
587/// `QasmError`: If there is an error generating, parsing, or analyzing the `OpenQASM` source.
588/// `QSharpError`: If there is an error evaluating the program.
589/// `QSharpError`: If there is an error synthesizing the circuit.
590#[pyfunction]
591#[allow(clippy::too_many_arguments)]
592#[pyo3(
593 signature = (source, config, read_file, list_directory, resolve_path, fetch_github, **kwargs)
594)]
595pub(crate) fn circuit_qasm_program(
596 py: Python,
597 source: &str,
598 config: &CircuitConfig,
599 read_file: Option<Py<PyAny>>,
600 list_directory: Option<Py<PyAny>>,
601 resolve_path: Option<Py<PyAny>>,
602 fetch_github: Option<Py<PyAny>>,
603 kwargs: Option<Bound<'_, PyDict>>,
604) -> PyResult<Py<PyAny>> {
605 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
606
607 let operation_name = get_operation_name(&kwargs)?;
608 let search_path = get_search_path(&kwargs)?;
609
610 let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
611 let file_path = PathBuf::from_str(&search_path)
612 .expect("from_str is infallible")
613 .join("program.qasm");
614 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(source)));
615 let ProjectType::OpenQASM(sources) = project.project_type else {
616 return Err(QasmError::new_err(
617 "Expected OpenQASM project, but got a different type".to_string(),
618 ));
619 };
620 let res = qsc::openqasm::semantic::parse_sources(&sources);
621
622 let (package, source_map, signature) = compile_qasm_enriching_errors(
623 res,
624 &operation_name,
625 ProgramType::File,
626 OutputSemantics::ResourceEstimation,
627 false,
628 )?;
629
630 let package_type = PackageType::Exe;
631 let language_features = LanguageFeatures::default();
632 let target_profile = if matches!(
633 config.generation_method,
634 Some(crate::interpreter::CircuitGenerationMethod::Static)
635 ) {
636 TargetProfile::Adaptive_RIF.into()
637 } else {
638 TargetProfile::Unrestricted.into()
639 };
640
641 let mut interpreter = create_interpreter_from_ast(
642 package,
643 source_map,
644 target_profile,
645 language_features,
646 package_type,
647 )
648 .map_err(|errors| QSharpError::new_err(format_errors(errors)))?;
649
650 let entry_expr = signature.create_entry_expr_from_params(String::new());
651 interpreter
652 .set_entry_expr(&entry_expr)
653 .map_err(|errors| map_entry_compilation_errors(errors, &signature))?;
654
655 let tracer_config = qsc::circuit::TracerConfig {
656 max_operations: config
657 .max_operations
658 .unwrap_or(TracerConfig::DEFAULT_MAX_OPERATIONS),
659 source_locations: config.source_locations,
660 group_by_scope: config.group_by_scope,
661 prune_classical_qubits: config.prune_classical_qubits,
662 };
663
664 let generation_method = if let Some(generation_method) = config.generation_method {
665 generation_method.into()
666 } else {
667 qsc::interpret::CircuitGenerationMethod::ClassicalEval
668 };
669
670 match interpreter.circuit(
671 CircuitEntryPoint::EntryExpr(entry_expr),
672 generation_method,
673 tracer_config,
674 ) {
675 Ok(circuit) => crate::interpreter::Circuit(circuit).into_py_any(py),
676 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
677 }
678}
679
680/// Converts a list of Q# errors into a list of resource estimator errors.
681fn into_estimation_errors(errors: Vec<interpret::Error>) -> Vec<resource_estimator::Error> {
682 errors
683 .into_iter()
684 .map(|error| resource_estimator::Error::Interpreter(error.clone()))
685 .collect::<Vec<_>>()
686}
687
688/// Formats a list of QASM errors into a single string.
689pub(crate) fn format_qasm_errors(errors: Vec<WithSource<qsc::openqasm::error::Error>>) -> String {
690 errors
691 .into_iter()
692 .map(|e| {
693 let mut message = String::new();
694 let report = miette::Report::new(e);
695 write!(message, "{report:?}").expect("write should succeed");
696 message
697 })
698 .collect::<String>()
699}
700
701/// Creates a `FileSystem` from the provided Python callbacks.
702/// If any of the callbacks are missing, this will panic.
703pub(crate) fn create_filesystem_from_py(
704 py: Python,
705 read_file: Option<Py<PyAny>>,
706 list_directory: Option<Py<PyAny>>,
707 resolve_path: Option<Py<PyAny>>,
708 fetch_github: Option<Py<PyAny>>,
709) -> impl FileSystem {
710 file_system(
711 py,
712 read_file.expect("file system hooks should have been passed in with a read file callback"),
713 list_directory
714 .expect("file system hooks should have been passed in with a list directory callback"),
715 resolve_path
716 .expect("file system hooks should have been passed in with a resolve path callback"),
717 fetch_github
718 .expect("file system hooks should have been passed in with a fetch github callback"),
719 )
720}
721
722/// Creates an `Interpreter` from the provided AST package and configuration.
723fn create_interpreter_from_ast(
724 ast_package: Package,
725 source_map: SourceMap,
726 profile: Profile,
727 language_features: LanguageFeatures,
728 package_type: PackageType,
729) -> Result<Interpreter, Vec<interpret::Error>> {
730 let capabilities = profile.into();
731 let (stdid, mut store) = qsc::compile::package_store_with_stdlib(capabilities);
732 let dependencies = vec![(PackageId::CORE, None), (stdid, None)];
733
734 let (mut unit, errors) = qsc::compile::compile_ast(
735 &store,
736 &dependencies,
737 ast_package,
738 source_map,
739 package_type,
740 capabilities,
741 );
742
743 if !errors.is_empty() {
744 return Err(into_errors(errors));
745 }
746
747 unit.expose();
748 let source_package_id = store.insert(unit);
749
750 interpret::Interpreter::with_package_store(
751 false,
752 store,
753 source_package_id,
754 capabilities,
755 language_features,
756 &dependencies,
757 )
758}
759
760/// Sanitizes the name to ensure it is a valid identifier according
761/// to the Q# specification. If the name is empty, returns "circuit".
762pub(crate) fn sanitize_name<S: AsRef<str>>(name: S) -> String {
763 let name = name.as_ref();
764 if name.is_empty() {
765 return "circuit".to_string();
766 }
767
768 let mut output = String::with_capacity(name.len());
769 let c = name.chars().next().expect("name should not be empty");
770 if c == '_' || c.is_alphabetic() {
771 output.push(c);
772 } else {
773 // invalid first character, replace with '_'
774 output.push('_');
775 }
776 output.extend(name.chars().skip(1).filter_map(|c| {
777 if c == '-' {
778 Some('_')
779 } else if c == '_' || c.is_alphanumeric() {
780 Some(c)
781 } else {
782 None
783 }
784 }));
785 output
786}
787
788/// Extracts the search path from the kwargs dictionary.
789/// If the search path is not present, returns an error.
790/// Otherwise, returns the search path as a string.
791pub(crate) fn get_search_path(kwargs: &Bound<'_, PyDict>) -> PyResult<String> {
792 kwargs.get_item("search_path")?.map_or_else(
793 || {
794 Err(PyException::new_err(
795 "Could not parse search path".to_string(),
796 ))
797 },
798 |x| x.extract::<String>(),
799 )
800}
801
802/// Extracts the program type from the kwargs dictionary.
803pub(crate) fn get_program_type<D>(kwargs: &Bound<'_, PyDict>, default: D) -> PyResult<ProgramType>
804where
805 D: FnOnce() -> ProgramType,
806{
807 match kwargs.get_item("program_type")? {
808 Some(obj) => Ok(obj.extract::<ProgramType>()?),
809 None => Ok(default()),
810 }
811}
812
813/// Extracts the output semantics from the kwargs dictionary.
814pub(crate) fn get_output_semantics<D>(
815 kwargs: &Bound<'_, PyDict>,
816 default: D,
817) -> PyResult<OutputSemantics>
818where
819 D: FnOnce() -> OutputSemantics,
820{
821 match kwargs.get_item("output_semantics")? {
822 Some(obj) => Ok(obj.extract::<OutputSemantics>()?),
823 None => Ok(default()),
824 }
825}
826
827/// Extracts the output semantics from the kwargs dictionary.
828pub(crate) fn get_sim_type(kwargs: &Bound<'_, PyDict>) -> PyResult<SimType> {
829 match kwargs.get_item("sim_type")? {
830 Some(obj) => Ok(match obj.extract::<String>()?.as_str() {
831 "sparse" => SimType::Sparse,
832 "clifford" => {
833 // Clifford simulator needs a num_qubits, which defaults to 1000 if unspecified.
834 let num_qubits = kwargs
835 .get_item("num_qubits")?
836 .map_or_else(|| Ok(1000), |x| x.extract::<usize>())?;
837 SimType::Clifford(num_qubits)
838 }
839 other => {
840 return Err(PyException::new_err(format!(
841 "Invalid sim type specified: {other}"
842 )));
843 }
844 }),
845 None => Ok(Default::default()),
846 }
847}
848
849/// Extracts the name from the kwargs dictionary.
850/// If the name is not present, returns "program".
851/// Otherwise, returns the name after sanitizing it.
852pub(crate) fn get_operation_name(kwargs: &Bound<'_, PyDict>) -> PyResult<String> {
853 let name = kwargs
854 .get_item("name")?
855 .map_or_else(|| Ok("program".to_string()), |x| x.extract::<String>())?;
856
857 // sanitize the name to ensure it is a valid identifier
858 // When creating operation, we'll throw an error if the name is not a valid identifier
859 // so that the user gets the exact name they expect, but here it's better to sanitize.
860 Ok(sanitize_name(name))
861}
862
863/// Extracts the target profile from the kwargs dictionary.
864/// If the target profile is not present, returns `TargetProfile::Unrestricted`.
865/// Otherwise if not a valid `TargetProfile`, returns an error.
866///
867/// This also maps the `TargetProfile` exposed to Python to a `Profile`
868/// used by the interpreter.
869pub(crate) fn get_target_profile(kwargs: &Bound<'_, PyDict>) -> PyResult<Profile> {
870 match kwargs.get_item("target_profile")? {
871 Some(obj) => Ok(obj.extract::<TargetProfile>()?.into()),
872 None => Ok(TargetProfile::Unrestricted.into()),
873 }
874}
875
876/// Extracts the shots from the kwargs dictionary.
877/// If the shots are not present, or are not a valid usize, returns an error.
878pub(crate) fn get_shots(kwargs: &Bound<'_, PyDict>) -> PyResult<usize> {
879 kwargs.get_item("shots")?.map_or_else(
880 || Err(PyException::new_err("Could not parse shots".to_string())),
881 |x| x.extract::<usize>(),
882 )
883}
884
885/// Extracts the seed from the kwargs dictionary.
886/// If the seed is not present, or is not a valid u64, returns None.
887pub(crate) fn get_seed(kwargs: &Bound<'_, PyDict>) -> Option<u64> {
888 kwargs
889 .get_item("seed")
890 .ok()?
891 .map_or_else(|| None::<u64>, |x| x.extract::<u64>().ok())
892}
893