quantrs2_device/neutral_atom/
client.rs1use crate::{DeviceError, DeviceResult};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::Duration;
10use tokio::time::timeout;
11
12#[derive(Debug, Clone)]
14pub struct NeutralAtomClient {
15 pub base_url: String,
17 pub auth_token: String,
19 pub client: reqwest::Client,
21 pub timeout: Duration,
23 pub headers: HashMap<String, String>,
25}
26
27impl NeutralAtomClient {
28 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 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 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 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 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 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 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 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 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 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 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct NeutralAtomJobRequest {
213 pub device_id: String,
214 pub circuit: String, 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#[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#[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#[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}