quantrs2_device/neutral_atom/
client.rs

1//! Neutral atom quantum computing client implementation
2//!
3//! This module provides client connectivity for neutral atom quantum computers,
4//! supporting various neutral atom platforms and hardware providers.
5
6use crate::{DeviceError, DeviceResult};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::Duration;
10use tokio::time::timeout;
11
12/// Client for neutral atom quantum computing systems
13#[derive(Debug, Clone)]
14pub struct NeutralAtomClient {
15    /// Base URL for the neutral atom service
16    pub base_url: String,
17    /// Authentication token
18    pub auth_token: String,
19    /// HTTP client for API requests
20    pub client: reqwest::Client,
21    /// Request timeout
22    pub timeout: Duration,
23    /// Additional headers for requests
24    pub headers: HashMap<String, String>,
25}
26
27impl NeutralAtomClient {
28    /// Create a new neutral atom client
29    pub fn new(base_url: String, auth_token: String) -> DeviceResult<Self> {
30        let client = reqwest::Client::builder()
31            .timeout(Duration::from_secs(30))
32            .build()
33            .map_err(|e| DeviceError::Connection(format!("Failed to create HTTP client: {e}")))?;
34
35        Ok(Self {
36            base_url,
37            auth_token,
38            client,
39            timeout: Duration::from_secs(300),
40            headers: HashMap::new(),
41        })
42    }
43
44    /// Create a new neutral atom client with custom configuration
45    pub fn with_config(
46        base_url: String,
47        auth_token: String,
48        timeout_secs: u64,
49        headers: HashMap<String, String>,
50    ) -> DeviceResult<Self> {
51        let client = reqwest::Client::builder()
52            .timeout(Duration::from_secs(timeout_secs))
53            .build()
54            .map_err(|e| DeviceError::Connection(format!("Failed to create HTTP client: {e}")))?;
55
56        Ok(Self {
57            base_url,
58            auth_token,
59            client,
60            timeout: Duration::from_secs(timeout_secs),
61            headers,
62        })
63    }
64
65    /// Get available neutral atom devices
66    pub async fn get_devices(&self) -> DeviceResult<Vec<NeutralAtomDeviceInfo>> {
67        let url = format!("{}/devices", self.base_url);
68        let response = timeout(self.timeout, self.get_request(&url))
69            .await
70            .map_err(|_| DeviceError::Timeout("Request timed out".to_string()))?
71            .map_err(|e| DeviceError::APIError(format!("Failed to get devices: {e}")))?;
72
73        response
74            .json::<Vec<NeutralAtomDeviceInfo>>()
75            .await
76            .map_err(|e| DeviceError::Deserialization(format!("Failed to parse devices: {e}")))
77    }
78
79    /// Get device information by ID
80    pub async fn get_device(&self, device_id: &str) -> DeviceResult<NeutralAtomDeviceInfo> {
81        let url = format!("{}/devices/{}", self.base_url, device_id);
82        let response = timeout(self.timeout, self.get_request(&url))
83            .await
84            .map_err(|_| DeviceError::Timeout("Request timed out".to_string()))?
85            .map_err(|e| DeviceError::APIError(format!("Failed to get device: {e}")))?;
86
87        response
88            .json::<NeutralAtomDeviceInfo>()
89            .await
90            .map_err(|e| DeviceError::Deserialization(format!("Failed to parse device: {e}")))
91    }
92
93    /// Submit a job to a neutral atom device
94    pub async fn submit_job(&self, job_request: &NeutralAtomJobRequest) -> DeviceResult<String> {
95        let url = format!("{}/jobs", self.base_url);
96        let response = timeout(self.timeout, self.post_request(&url, job_request))
97            .await
98            .map_err(|_| DeviceError::Timeout("Request timed out".to_string()))?
99            .map_err(|e| DeviceError::JobSubmission(format!("Failed to submit job: {e}")))?;
100
101        let job_response: NeutralAtomJobResponse = response.json().await.map_err(|e| {
102            DeviceError::Deserialization(format!("Failed to parse job response: {e}"))
103        })?;
104
105        Ok(job_response.job_id)
106    }
107
108    /// Get job status
109    pub async fn get_job_status(&self, job_id: &str) -> DeviceResult<NeutralAtomJobStatus> {
110        let url = format!("{}/jobs/{}", self.base_url, job_id);
111        let response = timeout(self.timeout, self.get_request(&url))
112            .await
113            .map_err(|_| DeviceError::Timeout("Request timed out".to_string()))?
114            .map_err(|e| DeviceError::APIError(format!("Failed to get job status: {e}")))?;
115
116        response
117            .json::<NeutralAtomJobStatus>()
118            .await
119            .map_err(|e| DeviceError::Deserialization(format!("Failed to parse job status: {e}")))
120    }
121
122    /// Get job results
123    pub async fn get_job_results(&self, job_id: &str) -> DeviceResult<NeutralAtomJobResult> {
124        let url = format!("{}/jobs/{}/results", self.base_url, job_id);
125        let response = timeout(self.timeout, self.get_request(&url))
126            .await
127            .map_err(|_| DeviceError::Timeout("Request timed out".to_string()))?
128            .map_err(|e| DeviceError::APIError(format!("Failed to get job results: {e}")))?;
129
130        response
131            .json::<NeutralAtomJobResult>()
132            .await
133            .map_err(|e| DeviceError::Deserialization(format!("Failed to parse job results: {e}")))
134    }
135
136    /// Cancel a job
137    pub async fn cancel_job(&self, job_id: &str) -> DeviceResult<()> {
138        let url = format!("{}/jobs/{}/cancel", self.base_url, job_id);
139        timeout(self.timeout, self.delete_request(&url))
140            .await
141            .map_err(|_| DeviceError::Timeout("Request timed out".to_string()))?
142            .map_err(|e| DeviceError::APIError(format!("Failed to cancel job: {e}")))?;
143
144        Ok(())
145    }
146
147    /// Perform GET request
148    async fn get_request(&self, url: &str) -> Result<reqwest::Response, reqwest::Error> {
149        let mut request = self.client.get(url).bearer_auth(&self.auth_token);
150
151        for (key, value) in &self.headers {
152            request = request.header(key, value);
153        }
154
155        request.send().await?.error_for_status()
156    }
157
158    /// Perform POST request
159    async fn post_request<T: Serialize>(
160        &self,
161        url: &str,
162        body: &T,
163    ) -> Result<reqwest::Response, reqwest::Error> {
164        let mut request = self
165            .client
166            .post(url)
167            .bearer_auth(&self.auth_token)
168            .json(body);
169
170        for (key, value) in &self.headers {
171            request = request.header(key, value);
172        }
173
174        request.send().await?.error_for_status()
175    }
176
177    /// Perform DELETE request
178    async fn delete_request(&self, url: &str) -> Result<reqwest::Response, reqwest::Error> {
179        let mut request = self.client.delete(url).bearer_auth(&self.auth_token);
180
181        for (key, value) in &self.headers {
182            request = request.header(key, value);
183        }
184
185        request.send().await?.error_for_status()
186    }
187}
188
189/// Information about a neutral atom device
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct NeutralAtomDeviceInfo {
192    pub id: String,
193    pub name: String,
194    pub provider: String,
195    pub system_type: String,
196    pub atom_count: usize,
197    pub atom_spacing: f64,
198    pub state_encoding: String,
199    pub blockade_radius: Option<f64>,
200    pub loading_efficiency: f64,
201    pub gate_fidelity: f64,
202    pub measurement_fidelity: f64,
203    pub is_available: bool,
204    pub queue_length: usize,
205    pub estimated_wait_time: Option<Duration>,
206    pub capabilities: Vec<String>,
207    pub properties: HashMap<String, String>,
208}
209
210/// Request to submit a job to a neutral atom device
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct NeutralAtomJobRequest {
213    pub device_id: String,
214    pub circuit: String, // Serialized circuit
215    pub shots: usize,
216    pub config: Option<HashMap<String, serde_json::Value>>,
217    pub priority: Option<String>,
218    pub tags: Option<HashMap<String, String>>,
219}
220
221/// Response from job submission
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct NeutralAtomJobResponse {
224    pub job_id: String,
225    pub status: String,
226    pub estimated_execution_time: Option<Duration>,
227    pub queue_position: Option<usize>,
228}
229
230/// Status of a neutral atom job
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct NeutralAtomJobStatus {
233    pub job_id: String,
234    pub status: String,
235    pub created_at: String,
236    pub started_at: Option<String>,
237    pub completed_at: Option<String>,
238    pub progress: Option<f64>,
239    pub queue_position: Option<usize>,
240    pub estimated_completion: Option<String>,
241    pub error_message: Option<String>,
242}
243
244/// Results from a neutral atom job
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct NeutralAtomJobResult {
247    pub job_id: String,
248    pub device_id: String,
249    pub status: String,
250    pub results: HashMap<String, serde_json::Value>,
251    pub metadata: HashMap<String, String>,
252    pub execution_time: Duration,
253    pub shots_completed: usize,
254    pub fidelity_estimate: Option<f64>,
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_neutral_atom_client_creation() {
263        let client = NeutralAtomClient::new(
264            "https://api.neutralatom.example.com".to_string(),
265            "test_token".to_string(),
266        );
267        assert!(client.is_ok());
268    }
269
270    #[test]
271    fn test_neutral_atom_client_with_config() {
272        let mut headers = HashMap::new();
273        headers.insert("User-Agent".to_string(), "QuantRS2".to_string());
274
275        let client = NeutralAtomClient::with_config(
276            "https://api.neutralatom.example.com".to_string(),
277            "test_token".to_string(),
278            60,
279            headers,
280        );
281        assert!(client.is_ok());
282    }
283
284    #[test]
285    fn test_neutral_atom_device_info_serialization() {
286        let device_info = NeutralAtomDeviceInfo {
287            id: "neutral_atom_1".to_string(),
288            name: "Test Neutral Atom Device".to_string(),
289            provider: "TestProvider".to_string(),
290            system_type: "Rydberg".to_string(),
291            atom_count: 100,
292            atom_spacing: 5.0,
293            state_encoding: "GroundExcited".to_string(),
294            blockade_radius: Some(8.0),
295            loading_efficiency: 0.95,
296            gate_fidelity: 0.995,
297            measurement_fidelity: 0.99,
298            is_available: true,
299            queue_length: 0,
300            estimated_wait_time: None,
301            capabilities: vec!["rydberg_gates".to_string()],
302            properties: HashMap::new(),
303        };
304
305        let serialized = serde_json::to_string(&device_info);
306        assert!(serialized.is_ok());
307
308        let deserialized: Result<NeutralAtomDeviceInfo, _> =
309            serde_json::from_str(&serialized.expect("serialization should succeed"));
310        assert!(deserialized.is_ok());
311    }
312}