qcs/executable.rs
1//! This module contains the public-facing API for executing programs. [`Executable`] is the how
2//! users will interact with QCS, quilc, and QVM.
3
4use std::borrow::Cow;
5use std::collections::HashMap;
6use std::num::NonZeroU16;
7use std::sync::Arc;
8use std::time::Duration;
9
10#[cfg(feature = "stubs")]
11use pyo3_stub_gen::derive::gen_stub_pyclass_enum;
12
13use qcs_api_client_common::configuration::LoadError;
14use quil_rs::quil::ToQuilError;
15
16use crate::client::Qcs;
17use crate::compiler::quilc::{self, CompilerOpts};
18use crate::execution_data::{self, ResultData};
19use crate::qpu::api::{ExecutionOptions, JobId};
20use crate::qpu::translation::TranslationOptions;
21use crate::qpu::ExecutionError;
22use crate::qvm::http::AddressRequest;
23use crate::{qpu, qvm};
24use quil_rs::program::ProgramError;
25
26/// The builder interface for executing Quil programs on QVMs and QPUs.
27///
28/// # Example
29///
30/// ```rust
31/// use qcs::client::Qcs;
32/// use qcs::Executable;
33/// use qcs::qvm;
34///
35///
36/// const PROGRAM: &str = r##"
37/// DECLARE ro BIT[2]
38///
39/// H 0
40/// CNOT 0 1
41///
42/// MEASURE 0 ro[0]
43/// MEASURE 1 ro[1]
44/// "##;
45///
46/// #[tokio::main]
47/// async fn main() {
48/// use std::num::NonZeroU16;
49/// use qcs::qvm;
50/// let qvm_client = qvm::http::HttpClient::from(&Qcs::load());
51/// let mut result = Executable::from_quil(PROGRAM).with_qcs_client(Qcs::default()).with_shots(NonZeroU16::new(4).unwrap()).execute_on_qvm(&qvm_client).await.unwrap();
52/// // "ro" is the only source read from by default if you don't specify a .read_from()
53///
54/// // We first convert the readout data to a [`RegisterMap`] to get a mapping of registers
55/// // (ie. "ro") to a [`RegisterMatrix`], `M`, where M[`shot`][`index`] is the value for
56/// // the memory offset `index` during shot `shot`.
57/// // There are some programs where QPU readout data does not fit into a [`RegisterMap`], in
58/// // which case you should build the matrix you need from [`QpuResultData`] directly. See
59/// // the [`RegisterMap`] documentation for more information on when this transformation
60/// // might fail.
61/// let data = result.result_data
62/// .to_register_map()
63/// .expect("should convert to readout map")
64/// .get_register_matrix("ro")
65/// .expect("should have data in ro")
66/// .as_integer()
67/// .expect("should be integer matrix")
68/// .to_owned();
69///
70/// // In this case, we ran the program for 4 shots, so we know the number of rows is 4.
71/// assert_eq!(data.nrows(), 4);
72/// for shot in data.rows() {
73/// // Each shot will contain all the memory, in order, for the vector (or "register") we
74/// // requested the results of. In this case, "ro" (the default).
75/// assert_eq!(shot.len(), 2);
76/// // In the case of this particular program, we know ro[0] should equal ro[1]
77/// assert_eq!(shot[0], shot[1]);
78/// }
79/// }
80///
81/// ```
82///
83/// # A Note on Lifetimes
84///
85/// This structure utilizes multiple lifetimes for the sake of runtime efficiency.
86/// You should be able to largely ignore these, just keep in mind that any borrowed data passed to
87/// the methods most likely needs to live as long as this struct. Check individual methods for
88/// specifics. If only using `'static` strings then everything should just work.
89#[derive(Clone)]
90#[allow(missing_debug_implementations)]
91pub struct Executable<'executable, 'execution> {
92 quil: Arc<str>,
93 shots: NonZeroU16,
94 readout_memory_region_names: Option<Vec<Cow<'executable, str>>>,
95 params: Parameters,
96 qcs_client: Option<Arc<Qcs>>,
97 quilc_client: Option<Arc<dyn quilc::Client + Send + Sync>>,
98 compiler_options: CompilerOpts,
99 qpu: Option<qpu::Execution<'execution>>,
100 qvm: Option<qvm::Execution>,
101}
102
103pub(crate) type Parameters = HashMap<Box<str>, Vec<f64>>;
104
105impl<'executable> Executable<'executable, '_> {
106 /// Create an [`Executable`] from a string containing a [quil](https://github.com/quil-lang/quil)
107 /// program. No additional work is done in this function, so the `quil` may actually be invalid.
108 ///
109 /// The constructed [`Executable`] defaults to "ro" as a read-out register and 1 for the number
110 /// of shots. Those can be overridden using [`Executable::read_from`] and
111 /// [`Executable::with_shots`] respectively.
112 ///
113 /// Note that changing the program for an associated [`Executable`] is not allowed, you'll have to
114 /// create a new [`Executable`] if you want to run a different program.
115 ///
116 /// # Arguments
117 ///
118 /// 1. `quil` is a string slice representing the original program to be run. The returned
119 /// [`Executable`] will only live as long as this reference.
120 #[must_use]
121 #[allow(clippy::missing_panics_doc)]
122 pub fn from_quil<Quil: Into<Arc<str>>>(quil: Quil) -> Self {
123 Self {
124 quil: quil.into(),
125 shots: NonZeroU16::new(1).expect("value is non-zero"),
126 readout_memory_region_names: None,
127 params: Parameters::new(),
128 compiler_options: CompilerOpts::default(),
129 qpu: None,
130 qvm: None,
131 qcs_client: None,
132 quilc_client: None,
133 }
134 }
135
136 /// Specify a memory region or "register" to read results from. This must correspond to a
137 /// `DECLARE` statement in the provided Quil program. You can call this register multiple times
138 /// if you need to read multiple registers. If this method is never called, it's
139 /// assumed that a single register called "ro" is declared and should be read from.
140 ///
141 /// # Arguments
142 ///
143 /// 1. `register` is a string reference of the name of a register to read from. The lifetime
144 /// of this reference should be the lifetime of the [`Executable`], which is the lifetime of
145 /// the `quil` argument to [`Executable::from_quil`].
146 ///
147 /// # Example
148 ///
149 /// ```rust
150 /// use qcs::client::Qcs;
151 /// use qcs::Executable;
152 /// use qcs::qvm;
153 ///
154 /// const PROGRAM: &str = r#"
155 /// DECLARE first REAL[1]
156 /// DECLARE second REAL[1]
157 ///
158 /// MOVE first[0] 3.141
159 /// MOVE second[0] 1.234
160 /// "#;
161 ///
162 /// #[tokio::main]
163 /// async fn main() {
164 /// let qvm_client = qvm::http::HttpClient::from(&Qcs::load());
165 /// let mut result = Executable::from_quil(PROGRAM)
166 /// .with_qcs_client(Qcs::default()) // Unnecessary if you have ~/.qcs/settings.toml
167 /// .read_from("first")
168 /// .read_from("second")
169 /// .execute_on_qvm(&qvm_client)
170 /// .await
171 /// .unwrap();
172 /// let first_value = result
173 /// .result_data
174 /// .to_register_map()
175 /// .expect("qvm memory should fit readout map")
176 /// .get_register_matrix("first")
177 /// .expect("readout map should have 'first'")
178 /// .as_real()
179 /// .expect("should be real numbered register")
180 /// .get((0, 0))
181 /// .expect("should have value in first position of first register")
182 /// .clone();
183 /// let second_value = result
184 /// .result_data
185 /// .to_register_map()
186 /// .expect("qvm memory should fit readout map")
187 /// .get_register_matrix("second")
188 /// .expect("readout map should have 'second'")
189 /// .as_real()
190 /// .expect("should be real numbered register")
191 /// .get((0, 0))
192 /// .expect("should have value in first position of first register")
193 /// .clone();
194 /// assert_eq!(first_value, 3.141);
195 /// assert_eq!(second_value, 1.234);
196 /// }
197 /// ```
198 #[must_use]
199 pub fn read_from<S>(mut self, register: S) -> Self
200 where
201 S: Into<Cow<'executable, str>>,
202 {
203 let register = register.into();
204 #[cfg(feature = "tracing")]
205 tracing::trace!("reading from register {:?}", register);
206 let mut readouts = self.readout_memory_region_names.take().unwrap_or_default();
207 readouts.push(register);
208 self.readout_memory_region_names = Some(readouts);
209 self
210 }
211
212 /// Sets a concrete value for [parametric compilation].
213 /// The validity of parameters is not checked until execution.
214 ///
215 /// # Arguments
216 ///
217 /// 1. `param_name`: Reference to the name of the parameter which should correspond to a
218 /// `DECLARE` statement in the Quil program. The lifetime of the reference should be the
219 /// same as the [`Executable`]: that is the same as the `quil` param to [`Executable::from_quil`].
220 /// 2. `index`: The index into the memory vector that you're setting.
221 /// 3. `value`: The value to set for the specified memory.
222 ///
223 /// # Example
224 ///
225 /// ```rust
226 /// use qcs::client::Qcs;
227 /// use qcs::Executable;
228 /// use qcs::qvm;
229 ///
230 /// const PROGRAM: &str = "DECLARE theta REAL[2]";
231 ///
232 /// #[tokio::main]
233 /// async fn main() {
234 /// let qvm_client = qvm::http::HttpClient::from(&Qcs::load());
235 /// let mut exe = Executable::from_quil(PROGRAM)
236 /// .with_qcs_client(Qcs::default()) // Unnecessary if you have ~/.qcs/settings.toml
237 /// .read_from("theta");
238 ///
239 /// for theta in 0..2 {
240 /// let theta = theta as f64;
241 /// let mut result = exe
242 /// .with_parameter("theta", 0, theta)
243 /// .with_parameter("theta", 1, theta * 2.0)
244 /// .execute_on_qvm(&qvm_client).await.unwrap();
245 /// let theta_register = result
246 /// .result_data
247 /// .to_register_map()
248 /// .expect("should fit readout map")
249 /// .get_register_matrix("theta")
250 /// .expect("should have theta")
251 /// .as_real()
252 /// .expect("should be real valued register")
253 /// .to_owned();
254 ///
255 /// let first = theta_register
256 /// .get((0, 0))
257 /// .expect("first index, first shot of theta should have value")
258 /// .to_owned();
259 /// let second = theta_register
260 /// .get((0, 1))
261 /// .expect("first shot, second_index of theta should have value")
262 /// .to_owned();
263 ///
264 /// assert_eq!(first, theta);
265 /// assert_eq!(second, theta * 2.0);
266 /// }
267 /// }
268 /// ```
269 ///
270 /// [parametric compilation]: https://pyquil-docs.rigetti.com/en/stable/basics.html?highlight=parametric#parametric-compilation
271 pub fn with_parameter<Param: Into<Box<str>>>(
272 &mut self,
273 param_name: Param,
274 index: usize,
275 value: f64,
276 ) -> &mut Self {
277 let param_name = param_name.into();
278
279 #[cfg(feature = "tracing")]
280 tracing::trace!("setting parameter {}[{}] to {}", param_name, index, value);
281
282 let mut values = self
283 .params
284 .remove(¶m_name)
285 .unwrap_or_else(|| vec![0.0; index]);
286
287 if index >= values.len() {
288 values.resize(index + 1, 0.0);
289 }
290
291 values[index] = value;
292 self.params.insert(param_name, values);
293
294 self
295 }
296
297 /// Set the default configuration to be used when constructing clients
298 #[must_use]
299 pub fn with_qcs_client(mut self, client: Qcs) -> Self {
300 self.qcs_client = Some(Arc::from(client));
301 self
302 }
303
304 /// Get a reference to the [`Qcs`] client used by the executable.
305 ///
306 /// If one has not been set, a default client is loaded, set, and returned.
307 pub fn qcs_client(&mut self) -> Arc<Qcs> {
308 if let Some(client) = &self.qcs_client {
309 client.clone()
310 } else {
311 let client = Arc::new(Qcs::load());
312 self.qcs_client = Some(client.clone());
313 client
314 }
315 }
316}
317
318/// The [`Result`] from executing on a QPU or QVM.
319pub type ExecutionResult = Result<execution_data::ExecutionData, Error>;
320
321impl Executable<'_, '_> {
322 /// Specify a number of times to run the program for each execution. Defaults to 1 run or "shot".
323 #[must_use]
324 pub fn with_shots(mut self, shots: NonZeroU16) -> Self {
325 self.shots = shots;
326 self
327 }
328
329 /// Set the client used for compilation.
330 ///
331 /// To disable compilation, set this to `None`.
332 #[must_use]
333 #[allow(trivial_casts)]
334 pub fn with_quilc_client<C: quilc::Client + Send + Sync + 'static>(
335 mut self,
336 client: Option<C>,
337 ) -> Self {
338 self.quilc_client = client.map(|c| Arc::new(c) as _);
339 self
340 }
341
342 /// If set, the value will override the default compiler options
343 #[must_use]
344 pub fn compiler_options(mut self, options: CompilerOpts) -> Self {
345 self.compiler_options = options;
346 self
347 }
348
349 fn get_readouts(&self) -> &[Cow<'_, str>] {
350 self.readout_memory_region_names
351 .as_ref()
352 .map_or(&[Cow::Borrowed("ro")], Vec::as_slice)
353 }
354
355 /// Execute on a QVM which must be available at the configured URL (default <http://localhost:5000>).
356 ///
357 /// # Warning
358 ///
359 /// This function uses [`tokio::task::spawn_blocking`] internally. See the docs for that function
360 /// to avoid blocking shutdown of the runtime.
361 ///
362 /// # Returns
363 ///
364 /// An [`ExecutionResult`].
365 ///
366 /// # Errors
367 ///
368 /// See [`Error`].
369 pub async fn execute_on_qvm<V: qvm::Client + ?Sized>(&mut self, client: &V) -> ExecutionResult {
370 #[cfg(feature = "tracing")]
371 tracing::debug!(
372 num_shots = %self.shots,
373 "running Executable on QVM",
374 );
375
376 let qvm = if let Some(qvm) = self.qvm.take() {
377 qvm
378 } else {
379 qvm::Execution::new(&self.quil)?
380 };
381 let result = qvm
382 .run(
383 self.shots,
384 self.get_readouts()
385 .iter()
386 .map(|address| (address.to_string(), AddressRequest::IncludeAll()))
387 .collect(),
388 &self.params,
389 client,
390 )
391 .await;
392 self.qvm = Some(qvm);
393 result
394 .map_err(Error::from)
395 .map(|registers| execution_data::ExecutionData {
396 result_data: ResultData::Qvm(registers),
397 duration: None,
398 })
399 }
400}
401
402impl<'execution> Executable<'_, 'execution> {
403 /// Remove and return `self.qpu` if it's set and still valid. Otherwise, create a new one.
404 async fn qpu_for_id<S>(&mut self, id: S) -> Result<qpu::Execution<'execution>, Error>
405 where
406 S: Into<Cow<'execution, str>>,
407 {
408 let id = id.into();
409 if let Some(qpu) = self.qpu.take() {
410 if qpu.quantum_processor_id == id.as_ref() && qpu.shots == self.shots {
411 return Ok(qpu);
412 }
413 }
414 qpu::Execution::new(
415 self.quil.clone(),
416 self.shots,
417 id,
418 self.qcs_client(),
419 self.quilc_client.clone(),
420 self.compiler_options,
421 )
422 .await
423 .map_err(Error::from)
424 }
425
426 /// Compile the program and execute it on a QPU, waiting for results.
427 ///
428 /// # Arguments
429 /// 1. `quantum_processor_id`: The name of the QPU to run on. This parameter affects the
430 /// lifetime of the [`Executable`]. The [`Executable`] will only live as long as the last
431 /// parameter passed into this function.
432 ///
433 /// # Warning
434 ///
435 /// This function uses [`tokio::task::spawn_blocking`] internally. See the docs for that function
436 /// to avoid blocking shutdown of the runtime.
437 ///
438 /// # Returns
439 ///
440 /// An [`ExecutionResult`].
441 ///
442 /// # Errors
443 /// All errors are human readable by way of [`mod@thiserror`]. Some common errors are:
444 ///
445 /// 1. You are not authenticated for QCS
446 /// 1. Your credentials don't have an active reservation for the QPU you requested
447 /// 1. [quilc] was not running.
448 /// 1. The `quil` that this [`Executable`] was constructed with was invalid.
449 /// 1. Missing parameters that should be filled with [`Executable::with_parameter`]
450 ///
451 /// [quilc]: https://github.com/quil-lang/quilc
452 pub async fn execute_on_qpu_with_endpoint<S>(
453 &mut self,
454 quantum_processor_id: S,
455 endpoint_id: S,
456 translation_options: Option<TranslationOptions>,
457 ) -> ExecutionResult
458 where
459 S: Into<Cow<'execution, str>>,
460 {
461 let job_handle = self
462 .submit_to_qpu_with_endpoint(quantum_processor_id, endpoint_id, translation_options)
463 .await?;
464 self.retrieve_results(job_handle).await
465 }
466
467 /// Compile the program and execute it on a QCS endpoint, waiting for results.
468 ///
469 /// # Arguments
470 /// 1. `quantum_processor_id`: The ID of the QPU for which to translate the program.
471 /// This parameter affects the lifetime of the [`Executable`].
472 /// The [`Executable`] will only live as long as the last parameter passed into this function.
473 /// 2. `translation_options`: An optional [`TranslationOptions`] that is used to configure how
474 /// the program in translated.
475 /// 3. `execution_options`: The [`ExecutionOptions`] to use. If the connection strategy used
476 /// is [`crate::qpu::api::ConnectionStrategy::EndpointId`] or
477 /// [`crate::qpu::api::ConnectionStrategy::EndpointAddress`] then direct access to that endpoint
478 /// overrides the `quantum_processor_id` parameter.
479 ///
480 /// # Warning
481 ///
482 /// This function uses [`tokio::task::spawn_blocking`] internally. See the docs for that function
483 /// to avoid blocking shutdown of the runtime.
484 ///
485 /// # Returns
486 ///
487 /// An [`ExecutionResult`].
488 ///
489 /// # Errors
490 /// All errors are human readable by way of [`mod@thiserror`]. Some common errors are:
491 ///
492 /// 1. You are not authenticated for QCS
493 /// 1. Your credentials don't have an active reservation for the QPU you requested
494 /// 1. [quilc] was not running.
495 /// 1. The `quil` that this [`Executable`] was constructed with was invalid.
496 /// 1. Missing parameters that should be filled with [`Executable::with_parameter`]
497 ///
498 /// [quilc]: https://github.com/quil-lang/quilc
499 pub async fn execute_on_qpu<S>(
500 &mut self,
501 quantum_processor_id: S,
502 translation_options: Option<TranslationOptions>,
503 execution_options: &ExecutionOptions,
504 ) -> ExecutionResult
505 where
506 S: Into<Cow<'execution, str>>,
507 {
508 let quantum_processor_id = quantum_processor_id.into();
509
510 #[cfg(feature = "tracing")]
511 tracing::debug!(
512 num_shots = %self.shots,
513 %quantum_processor_id,
514 "running Executable on QPU",
515 );
516
517 let job_handle = self
518 .submit_to_qpu(quantum_processor_id, translation_options, execution_options)
519 .await?;
520 self.retrieve_results(job_handle).await
521 }
522
523 /// Compile and submit the program to a QPU, but do not wait for execution to complete.
524 ///
525 /// Call [`Executable::retrieve_results`] to wait for execution to complete and retrieve the
526 /// results.
527 ///
528 /// # Arguments
529 /// 1. `quantum_processor_id`: The ID of the QPU for which to translate the program.
530 /// This parameter affects the lifetime of the [`Executable`].
531 /// The [`Executable`] will only live as long as the last parameter passed into this function.
532 /// 2. `translation_options`: An optional [`TranslationOptions`] that is used to configure how
533 /// the program in translated.
534 /// 3. `execution_options`: The [`ExecutionOptions`] to use. If the connection strategy used
535 /// is [`crate::qpu::api::ConnectionStrategy::EndpointId`] or
536 /// [`crate::qpu::api::ConnectionStrategy::EndpointAddress`] then direct access to that endpoint
537 /// overrides the `quantum_processor_id` parameter.
538 ///
539 /// # Errors
540 ///
541 /// See [`Executable::execute_on_qpu`].
542 pub async fn submit_to_qpu<S>(
543 &mut self,
544 quantum_processor_id: S,
545 translation_options: Option<TranslationOptions>,
546 execution_options: &ExecutionOptions,
547 ) -> Result<JobHandle<'execution>, Error>
548 where
549 S: Into<Cow<'execution, str>>,
550 {
551 let quantum_processor_id = quantum_processor_id.into();
552
553 #[cfg(feature = "tracing")]
554 tracing::debug!(
555 num_shots = %self.shots,
556 %quantum_processor_id,
557 "submitting Executable to QPU",
558 );
559
560 let job_handle = self
561 .qpu_for_id(quantum_processor_id)
562 .await?
563 .submit(&self.params, translation_options, execution_options)
564 .await?;
565 Ok(job_handle)
566 }
567
568 /// Compile and submit the program to a QCS endpoint, but do not wait for execution to complete.
569 ///
570 /// Call [`Executable::retrieve_results`] to wait for execution to complete and retrieve the
571 /// results.
572 ///
573 /// # Errors
574 ///
575 /// See [`Executable::execute_on_qpu`].
576 pub async fn submit_to_qpu_with_endpoint<S>(
577 &mut self,
578 quantum_processor_id: S,
579 endpoint_id: S,
580 translation_options: Option<TranslationOptions>,
581 ) -> Result<JobHandle<'execution>, Error>
582 where
583 S: Into<Cow<'execution, str>>,
584 {
585 let job_handle = self
586 .qpu_for_id(quantum_processor_id)
587 .await?
588 .submit_to_endpoint_id(&self.params, endpoint_id.into(), translation_options)
589 .await?;
590 Ok(job_handle)
591 }
592
593 /// Cancel a job that has yet to begin executing.
594 ///
595 /// This action is *not* atomic, and will attempt to cancel a job even if it cannot be cancelled. A
596 /// job can be cancelled only if it has not yet started executing.
597 ///
598 /// Success response indicates only that the request was received. Cancellation is not guaranteed,
599 /// as it is based on job state at the time of cancellation, and is completed on a best effort
600 /// basis.
601 pub async fn cancel_qpu_job(&mut self, job_handle: JobHandle<'execution>) -> Result<(), Error> {
602 let quantum_processor_id = job_handle.quantum_processor_id.to_string();
603 let qpu = self.qpu_for_id(quantum_processor_id).await?;
604 Ok(qpu.cancel_job(job_handle).await?)
605 }
606
607 /// Wait for the results of a job submitted via [`Executable::submit_to_qpu`] to complete.
608 ///
609 /// # Errors
610 ///
611 /// See [`Executable::execute_on_qpu`].
612 pub async fn retrieve_results(&mut self, job_handle: JobHandle<'execution>) -> ExecutionResult {
613 let quantum_processor_id = job_handle.quantum_processor_id.to_string();
614 let qpu = self.qpu_for_id(quantum_processor_id).await?;
615 qpu.retrieve_results(job_handle).await.map_err(Error::from)
616 }
617}
618
619/// The possible errors which can be returned by [`Executable::execute_on_qpu`] and
620/// [`Executable::execute_on_qvm`]..
621#[derive(Debug, thiserror::Error)]
622pub enum Error {
623 /// Communicating with QCS requires appropriate settings and secrets files. By default, these
624 /// should be `$HOME/.qcs/settings.toml` and `$HOME/.qcs/secrets.toml`, though those files can
625 /// be overridden by setting the `QCS_SETTINGS_FILE_PATH` and `QCS_SECRETS_FILE_PATH`
626 /// environment variables.
627 ///
628 /// This error can occur when one of those files is required but missing or there is a problem
629 /// with the contents of those files.
630 #[error("There was a problem related to your QCS settings: {0}")]
631 Settings(String),
632 /// This error occurs when the SDK was unable to authenticate a request to QCS. This could mean
633 /// that your credentials are invalid or expired, or that you do not have access to the requested
634 /// QPU.
635 #[error("Could not authenticate a request to QCS for the requested QPU.")]
636 Authentication,
637 /// An API error occurred while connecting to the QPU.
638 #[error("An API error occurred while connecting to the QPU: {0}")]
639 QpuApiError(#[from] qpu::api::QpuApiError),
640 /// This happens when the QPU is down for maintenance and not accepting new jobs. If you receive
641 /// this error, internal compilation caches will have been cleared as programs should be recompiled
642 /// with new settings after a maintenance window. If you are mid-experiment, you might want to
643 /// start over.
644 #[error("QPU currently unavailable, retry after {} seconds", .0.as_secs())]
645 QpuUnavailable(Duration),
646 /// Indicates a problem connecting to an external service. Check your network connection and
647 /// ensure that any required local services (e.g., `qvm` or `quilc`) are running.
648 #[error("Error connecting to service {0:?}")]
649 Connection(Service),
650 /// There was some problem with the provided Quil program. This could be a syntax error with
651 /// quil, providing Quil-T to `quilc` or `qvm` (which is not supported), or forgetting to set
652 /// some parameters.
653 #[error("There was a problem with the Quil program: {0}")]
654 Quil(#[from] ProgramError),
655 /// There was some problem converting the provided Quil program to valid Quil.
656 #[error("There was a problem converting the program to valid Quil: {0}")]
657 ToQuil(#[from] ToQuilError),
658 /// There was a problem when compiling the Quil program.
659 #[error("There was a problem compiling the Quil program: {0}")]
660 Compilation(String),
661 /// There was a problem when translating the Quil program.
662 #[error("There was a problem translating the Quil program: {0}")]
663 Translation(String),
664 /// There was a problem when substituting parameters in the Quil program.
665 #[error("There was a problem substituting parameters in the Quil program: {0}")]
666 Substitution(String),
667 /// The Quil program is missing readout sources.
668 #[error("The Quil program is missing readout sources")]
669 MissingRoSources,
670 /// This error returns when a runtime check that _should_ always pass fails. This most likely
671 /// indicates a bug in the SDK and should be reported to
672 /// [GitHub](https://github.com/rigetti/qcs-sdk-rust/issues),
673 #[error("An unexpected error occurred, please open an issue on GitHub: {0:?}")]
674 Unexpected(String),
675 /// Occurs when [`Executable::retrieve_results`] is called with an invalid [`JobHandle`].
676 /// Calling functions on [`Executable`] between [`Executable::submit_to_qpu`] and
677 /// [`Executable::retrieve_results`] can invalidate the handle.
678 #[error("The job handle was not valid")]
679 InvalidJobHandle,
680 /// Occurs when failing to construct a [`Qcs`] client.
681 #[error("The QCS client configuration failed to load")]
682 QcsConfigLoadFailure(#[from] LoadError),
683}
684
685#[derive(Debug, Copy, Clone, Eq, PartialEq)]
686#[cfg_attr(feature = "stubs", gen_stub_pyclass_enum)]
687#[cfg_attr(
688 feature = "python",
689 pyo3::pyclass(module = "qcs_sdk", rename_all = "SCREAMING_SNAKE_CASE", eq)
690)]
691/// The external services that this SDK may connect to. Used to differentiate between networking
692/// issues in [`Error::Connection`].
693pub enum Service {
694 /// The open source [`quilc`](https://github.com/quil-lang/quilc) compiler.
695 ///
696 /// This compiler must be running before calling [`Executable::execute_on_qpu`] unless the
697 /// [`Executable::compile_with_quilc`] option is set to `false`. By default, it's assumed that
698 /// this is running on `tcp://localhost:5555`, but this can be overridden via
699 /// `[profiles.<profile_name>.applications.pyquil.quilc_url]` in your `.qcs/settings.toml` file.
700 Quilc,
701 /// The open source [`qvm`](https://github.com/quil-lang/qvm) simulator.
702 ///
703 /// This simulator must be running before calling [`Executable::execute_on_qvm`]. By default,
704 /// it's assumed that this is running on `http://localhost:5000`, but this can be overridden via
705 /// `[profiles.<profile_name>.applications.pyquil.qvm_url]` in your `.qcs/settings.toml` file.
706 Qvm,
707 /// The connection to [`QCS`](https://docs.rigetti.com/qcs/), the API for authentication,
708 /// QPU lookup, and translation.
709 ///
710 /// You should be able to reach this service as long as you have a connection to the internet.
711 Qcs,
712 /// The connection to the QPU itself. You can only connect to the QPU from an authorized network
713 /// (like QCS JupyterLab).
714 Qpu,
715}
716
717impl From<ExecutionError> for Error {
718 fn from(err: ExecutionError) -> Self {
719 match err {
720 ExecutionError::Unexpected(inner) => Self::Unexpected(format!("{inner:?}")),
721 ExecutionError::Quilc { .. } => Self::Connection(Service::Quilc),
722 ExecutionError::QcsClient(v) => Self::Unexpected(format!("{v:?}")),
723 ExecutionError::Translation(v) => Self::Translation(v.to_string()),
724 ExecutionError::Isa(v) => Self::Unexpected(format!("{v:?}")),
725 ExecutionError::ReadoutParse(v) => Self::Unexpected(format!("{v:?}")),
726 ExecutionError::Quil(e) => Self::Quil(e),
727 ExecutionError::ToQuil(e) => Self::ToQuil(e),
728 ExecutionError::Compilation { details } => Self::Compilation(details),
729 ExecutionError::RpcqClient(e) => Self::Unexpected(format!("{e:?}")),
730 ExecutionError::QpuApi(e) => Self::QpuApiError(e),
731 }
732 }
733}
734
735impl From<qvm::Error> for Error {
736 fn from(err: qvm::Error) -> Self {
737 match err {
738 qvm::Error::QvmCommunication { .. } | qvm::Error::Client { .. } => {
739 Self::Connection(Service::Qvm)
740 }
741 qvm::Error::ToQuil(q) => Self::ToQuil(q),
742 qvm::Error::Parsing(_)
743 | qvm::Error::ShotsMustBePositive
744 | qvm::Error::RegionSizeMismatch { .. }
745 | qvm::Error::RegionNotFound { .. }
746 | qvm::Error::Qvm { .. } => Self::Compilation(format!("{err}")),
747 }
748 }
749}
750
751/// The result of calling [`Executable::submit_to_qpu`]. Represents a quantum program running on
752/// a QPU. Can be passed to [`Executable::retrieve_results`] to retrieve the results of the job.
753#[derive(Debug, Clone, PartialEq, Eq)]
754pub struct JobHandle<'executable> {
755 job_id: JobId,
756 quantum_processor_id: Cow<'executable, str>,
757 endpoint_id: Option<Cow<'executable, str>>,
758 readout_map: HashMap<String, String>,
759 execution_options: ExecutionOptions,
760}
761
762impl<'a> JobHandle<'a> {
763 #[must_use]
764 pub(crate) fn new<S>(
765 job_id: JobId,
766 quantum_processor_id: S,
767 endpoint_id: Option<S>,
768 readout_map: HashMap<String, String>,
769 execution_options: ExecutionOptions,
770 ) -> Self
771 where
772 S: Into<Cow<'a, str>>,
773 {
774 Self {
775 job_id,
776 quantum_processor_id: quantum_processor_id.into(),
777 endpoint_id: endpoint_id.map(Into::into),
778 readout_map,
779 execution_options,
780 }
781 }
782
783 /// The string representation of the QCS Job ID. Useful for debugging.
784 #[must_use]
785 pub fn job_id(&self) -> JobId {
786 self.job_id.clone()
787 }
788
789 /// The ID of the quantum processor to which the job was submitted.
790 #[must_use]
791 pub fn quantum_processor_id(&self) -> &str {
792 &self.quantum_processor_id
793 }
794
795 /// The readout map from source readout memory locations to the
796 /// filter pipeline node which publishes the data.
797 #[must_use]
798 pub fn readout_map(&self) -> &HashMap<String, String> {
799 &self.readout_map
800 }
801
802 /// The [`ExecutionOptions`] used to submit the job to the QPU.
803 #[must_use]
804 pub fn execution_options(&self) -> &ExecutionOptions {
805 &self.execution_options
806 }
807}
808
809#[cfg(test)]
810#[cfg(feature = "manual-tests")]
811mod describe_get_config {
812 use crate::client::Qcs;
813 use crate::{compiler::rpcq, Executable};
814
815 fn quilc_client() -> rpcq::Client {
816 let qcs = Qcs::load();
817 let endpoint = qcs.get_config().quilc_url();
818 rpcq::Client::new(endpoint).unwrap()
819 }
820
821 #[tokio::test]
822 async fn it_resizes_params_dynamically() {
823 let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client()));
824
825 exe.with_parameter("foo", 0, 0.0);
826 let params = exe.params.get("foo").unwrap().len();
827 assert_eq!(params, 1);
828
829 exe.with_parameter("foo", 10, 10.0);
830 let params = exe.params.get("foo").unwrap().len();
831 assert_eq!(params, 11);
832 }
833}
834
835#[cfg(test)]
836#[cfg(feature = "manual-tests")]
837mod describe_qpu_for_id {
838 use assert2::let_assert;
839 use std::num::NonZeroU16;
840
841 use crate::compiler::quilc::CompilerOpts;
842 use crate::compiler::rpcq;
843 use crate::qpu;
844 use crate::{client::Qcs, Executable};
845
846 fn quilc_client() -> rpcq::Client {
847 let qcs = Qcs::load();
848 let endpoint = qcs.get_config().quilc_url();
849 rpcq::Client::new(endpoint).unwrap()
850 }
851
852 #[tokio::test]
853 async fn it_refreshes_auth_token() {
854 // Default config has no auth, so it should try to refresh
855 let mut exe = Executable::from_quil("")
856 .with_qcs_client(Qcs::load())
857 .with_quilc_client(Some(quilc_client()));
858 let result = exe.qpu_for_id("blah").await;
859 let Err(err) = result else {
860 panic!("Expected an error!");
861 };
862 let result_string = format!("{err:?}");
863 assert!(result_string.contains("refresh_token"));
864 }
865
866 #[tokio::test]
867 async fn it_loads_cached_version() {
868 let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client()));
869 let shots = NonZeroU16::new(17).expect("value is non-zero");
870 exe.shots = shots;
871 exe.qpu = Some(
872 qpu::Execution::new(
873 "".into(),
874 shots,
875 "Aspen-M-3".into(),
876 exe.qcs_client(),
877 exe.quilc_client.clone(),
878 CompilerOpts::default(),
879 )
880 .await
881 .unwrap(),
882 );
883 // Load config with no credentials to prevent creating a new Execution if it tries
884 let mut exe = exe.with_qcs_client(Qcs::default());
885
886 assert!(exe.qpu_for_id("Aspen-M-3").await.is_ok());
887 }
888
889 #[tokio::test]
890 async fn it_creates_new_after_shot_change() {
891 let original_shots = NonZeroU16::new(23).expect("value is non-zero");
892 let mut exe = Executable::from_quil("")
893 .with_quilc_client(Some(quilc_client()))
894 .with_shots(original_shots);
895 let qpu = exe.qpu_for_id("Aspen-9").await.unwrap();
896
897 assert_eq!(qpu.shots, original_shots);
898
899 // Cache so we can verify cache is not used.
900 exe.qpu = Some(qpu);
901 let new_shots = NonZeroU16::new(32).expect("value is non-zero");
902 exe = exe.with_shots(new_shots);
903 let qpu = exe.qpu_for_id("Aspen-9").await.unwrap();
904
905 assert_eq!(qpu.shots, new_shots);
906 }
907
908 #[tokio::test]
909 async fn it_creates_new_for_new_qpu_id() {
910 let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client()));
911 let qpu = exe.qpu_for_id("Aspen-9").await.unwrap();
912
913 assert_eq!(qpu.quantum_processor_id, "Aspen-9");
914
915 // Cache so we can verify cache is not used.
916 exe.qpu = Some(qpu);
917 // Load config with no credentials to prevent creating the new Execution (which would fail anyway)
918 let mut exe = exe.with_qcs_client(Qcs::default());
919 let result = exe.qpu_for_id("Aspen-8").await;
920
921 let_assert!(Err(crate::executable::Error::Unexpected(err)) = result);
922 assert!(err.contains("NoRefreshToken"));
923 assert!(exe.qpu.is_none());
924 }
925}