Skip to main content

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(&param_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}